11547 lines
478 KiB
JavaScript
11547 lines
478 KiB
JavaScript
// 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, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
// 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 = `<span style="margin-right: 10px; font-size: 1.1rem;">${icon}</span><span>${message}</span>`;
|
||
|
||
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 = `
|
||
<div class="custom-dialog-card">
|
||
<div class="custom-dialog-header">
|
||
<span>🔔</span>
|
||
<span>${escapeHtml(title)}</span>
|
||
</div>
|
||
<p class="custom-dialog-body">${escapeHtml(message)}</p>
|
||
<div class="custom-dialog-footer">
|
||
<button class="btn-dialog btn-dialog-confirm">OK</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<div class="custom-dialog-card">
|
||
<div class="custom-dialog-header">
|
||
<span>${isDanger ? '⚠️' : '❓'}</span>
|
||
<span>${escapeHtml(title)}</span>
|
||
</div>
|
||
<p class="custom-dialog-body">${escapeHtml(message)}</p>
|
||
<div class="custom-dialog-footer">
|
||
<button class="btn-dialog btn-dialog-cancel">Cancelar</button>
|
||
<button class="btn-dialog ${isDanger ? 'btn-dialog-danger' : 'btn-dialog-confirm'}">${isDanger ? 'Excluir' : 'Confirmar'}</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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 <nome_data_hora>.<extensao>
|
||
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 `
|
||
<div class="code-container">
|
||
<div class="code-header">
|
||
<span class="code-lang">${lang}</span>
|
||
<button class="btn-copy-code" onclick="copyCode(this)">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H5.25m11.9-3.664A2.251 2.251 0 0 0 15 2.25h-3a2.251 2.251 0 0 0-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5M16.5 9h3.75c.621 0 1.125.504 1.125 1.125v8.25c0 .621-.504 1.125-1.125 1.125H16.5m-3-12h3m-3 3h3M13.5 14.25h3" />
|
||
</svg>
|
||
<span>Copiar</span>
|
||
</button>
|
||
</div>
|
||
<pre><code class="hljs language-${lang}">${escapeHtml(codeContent)}</code></pre>
|
||
</div>
|
||
`;
|
||
};
|
||
|
||
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]) => `
|
||
<label class="temperature-option ${preset === key ? 'selected' : ''}" data-preset="${key}">
|
||
<input type="radio" name="temperature" value="${key}" ${preset === key ? 'checked' : ''}>
|
||
<div class="temperature-option-content">
|
||
<strong>${t.label}</strong>
|
||
<span>${t.description}</span>
|
||
</div>
|
||
</label>
|
||
`).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 = '<p style="color: var(--text-muted); text-align: center; padding: 20px;">Nenhum conhecimento aprendido ainda. A assistente aprende automaticamente quando você ensinar algo novo!</p>';
|
||
return;
|
||
}
|
||
knowledgeList.innerHTML = window.conhecimento.facts.map(f => `
|
||
<div class="knowledge-item" data-id="${f.id}">
|
||
<div class="knowledge-item-content" id="knowledge-content-${f.id}">
|
||
<span class="knowledge-source">${f.source}</span>
|
||
<p style="margin: 4px 0 0 0;">${f.content}</p>
|
||
<small style="display: block; margin-top: 4px;">${new Date(f.addedAt).toLocaleDateString('pt-BR')}</small>
|
||
</div>
|
||
<div class="knowledge-actions" id="knowledge-actions-${f.id}" style="display: flex; gap: 4px; align-self: flex-start; margin-left: 8px;">
|
||
<button class="knowledge-edit-btn" onclick="startEditKnowledge(${f.id})" title="Editar" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 2px 4px; font-size: 0.9rem; transition: color 0.2s;">✏️</button>
|
||
<button class="knowledge-delete-btn" onclick="deleteKnowledge(${f.id})" title="Excluir">×</button>
|
||
</div>
|
||
</div>
|
||
`).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 = `
|
||
<span class="knowledge-source">${fact.source}</span>
|
||
<textarea id="edit-textarea-${id}" style="width: 100%; min-height: 70px; padding: 8px; border-radius: 6px; border: 1px solid var(--brand-green); background: var(--bg-primary); color: var(--text-primary); font-family: var(--font-sans); font-size: 0.9rem; resize: vertical; margin: 6px 0; outline: none; box-shadow: 0 0 4px var(--brand-glow);">${fact.content}</textarea>
|
||
<div style="display: flex; gap: 8px; margin-top: 4px;">
|
||
<button onclick="saveEditKnowledge(${id})" style="background: var(--brand-green); border: none; color: white; padding: 4px 10px; border-radius: 6px; font-size: 0.75rem; cursor: pointer; font-weight: 500; transition: opacity 0.2s;">Salvar</button>
|
||
<button onclick="renderKnowledgeList()" style="background: var(--bg-tertiary); border: none; color: var(--text-primary); padding: 4px 10px; border-radius: 6px; font-size: 0.75rem; cursor: pointer; font-weight: 500; transition: opacity 0.2s;">Cancelar</button>
|
||
</div>
|
||
`;
|
||
|
||
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 => `
|
||
<div class="modal-tag-item ${selectedTagsState.includes(tag.id) ? 'selected' : ''}" data-tag="${tag.id}">
|
||
<input type="checkbox" ${selectedTagsState.includes(tag.id) ? 'checked' : ''}>
|
||
<span>${tag.label}</span>
|
||
</div>
|
||
`).join('');
|
||
|
||
modalTagList.querySelectorAll('.modal-tag-item').forEach(item => {
|
||
item.addEventListener('click', () => {
|
||
const tag = item.dataset.tag;
|
||
if (selectedTagsState.includes(tag)) {
|
||
selectedTagsState = selectedTagsState.filter(t => t !== tag);
|
||
} else {
|
||
selectedTagsState.push(tag);
|
||
}
|
||
renderTagModalList();
|
||
});
|
||
});
|
||
};
|
||
|
||
const renderSidebarTags = () => {
|
||
if (!sidebarTags) return;
|
||
let tagsHtml = `<button class="tag-filter ${activeTag === 'all' ? 'active' : ''}" data-tag="all">Todas</button>`;
|
||
tagsHtml += AVAILABLE_TAGS.map(tag => `
|
||
<button class="tag-filter ${activeTag === tag.id ? 'active' : ''}" data-tag="${tag.id}">
|
||
${tag.label}
|
||
</button>
|
||
`).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 = `<img src="${url}" alt="${file.name}">`;
|
||
} else {
|
||
previewHtml = `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" />
|
||
</svg>`;
|
||
}
|
||
|
||
item.innerHTML = `
|
||
${previewHtml}
|
||
<span class="file-name">${file.name}</span>
|
||
<button type="button" class="remove-attachment" data-index="${index}">×</button>
|
||
`;
|
||
|
||
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
|
||
? `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="pin-icon"><line x1="12" y1="17" x2="12" y2="22" stroke="currentColor" stroke-width="2" stroke-linecap="round"></line><path d="M5 17h14v-1.76a2 2 0 0 0-.44-1.24l-2.78-3.47A2 2 0 0 1 15 9.29V5a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4.29a2 2 0 0 1-.78 1.24L5.44 14a2 2 0 0 0-.44 1.24z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>`
|
||
: '';
|
||
|
||
const tagBadges = (chat.tags || []).map(tag =>
|
||
`<span class="history-tag-badge">${getTagEmoji(tag)}</span>`
|
||
).join('');
|
||
|
||
item.innerHTML = `
|
||
<div class="history-item-content">
|
||
${pinIcon}
|
||
<div class="history-item-title">${escapeHtml(chat.title)}</div>
|
||
${tagBadges ? `<div class="history-item-tags">${tagBadges}</div>` : ''}
|
||
</div>
|
||
<div class="history-item-actions">
|
||
<button class="btn-history-action btn-pin-chat" title="${chat.pinned ? 'Desafixar' : 'Fixar'}">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="${chat.pinned ? 'currentColor' : 'none'}" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||
<line x1="12" y1="17" x2="12" y2="22"></line>
|
||
<path d="M5 17h14v-1.76a2 2 0 0 0-.44-1.24l-2.78-3.47A2 2 0 0 1 15 9.29V5a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4.29a2 2 0 0 1-.78 1.24L5.44 14a2 2 0 0 0-.44 1.24z"></path>
|
||
</svg>
|
||
</button>
|
||
<button class="btn-history-action btn-tag-chat" title="Tags">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581a2.25 2.25 0 0 0 3.181 0l4.318-4.318a2.25 2.25 0 0 0 0-3.181l-9.58-9.581A2.25 2.25 0 0 0 9.568 3Z" />
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6Z" />
|
||
</svg>
|
||
</button>
|
||
<button class="btn-history-action btn-rename-chat" title="Renomear">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
||
</svg>
|
||
</button>
|
||
<button class="btn-history-action btn-delete-chat" title="Excluir">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
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 = '<div style="color: var(--text-muted); font-size: 12px; padding: 10px; text-align: center;">Nenhum chat salvo</div>';
|
||
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 = `<h3>${label}</h3>`;
|
||
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
|
||
? `<div class="avatar user-avatar user-avatar-img-wrapper"><img src="${window.agentConfig?.userAvatarUrl || window.agentConfig?.camilaAvatarUrl || 'assets/pedagog_user.png'}" alt="Usuário" class="user-avatar-img"></div>`
|
||
: `<div class="avatar bot-avatar bot-avatar-img-wrapper"><img src="${window.agentConfig?.iaAvatarUrl || window.agentConfig?.kemilyAvatarUrl || 'assets/pedagog_ia.png'}" alt="${window.agentConfig?.agentName || 'PedagogIA'}" class="bot-avatar-img"></div>`;
|
||
|
||
let contentHtml = '';
|
||
if (isUser) {
|
||
contentHtml = `<p>${escapeHtml(content).replace(/\n/g, '<br>')}</p>`;
|
||
} else {
|
||
if (media) {
|
||
if (media.type === 'image') {
|
||
contentHtml = `
|
||
<div class="media-message-card">
|
||
<div class="media-img-container">
|
||
<img src="${media.url}" alt="Ilustração gerada" class="generated-image" onload="scrollToBottom()">
|
||
</div>
|
||
<p class="media-caption">✨ "${escapeHtml(media.originalPrompt)}"</p>
|
||
<div class="media-actions-row">
|
||
<a href="${media.url}" download="${getDownloadFileName('imagem', 'png')}" target="_blank" class="media-download-btn">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||
</svg>
|
||
<span>Salvar na Galeria</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else if (media.type === 'audio') {
|
||
const audioId = 'audio_' + Date.now() + '_' + Math.floor(Math.random() * 10000);
|
||
contentHtml = `
|
||
<div class="media-message-card">
|
||
<div class="custom-audio-player">
|
||
<audio src="${media.url}" id="${audioId}"></audio>
|
||
<button class="audio-play-btn" onclick="toggleCustomAudio(this)" title="Ouvir música">
|
||
<svg class="play-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M8 5v14l11-7z"/>
|
||
</svg>
|
||
<svg class="pause-icon hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
||
</svg>
|
||
</button>
|
||
<div class="audio-timeline-container">
|
||
<div class="audio-timeline">
|
||
<div class="audio-progress"></div>
|
||
</div>
|
||
</div>
|
||
<span class="audio-time">0:00</span>
|
||
</div>
|
||
<p class="media-caption">🎵 "${escapeHtml(media.originalPrompt)}"</p>
|
||
<div class="media-actions-row">
|
||
<a href="${media.url}" download="${getDownloadFileName('musica', 'mp3')}" target="_blank" class="media-download-btn">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||
</svg>
|
||
<span>Baixar Música</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else if (media.type === 'video') {
|
||
contentHtml = `
|
||
<div class="media-message-card">
|
||
<div class="media-video-container">
|
||
<video controls src="${media.url}" class="generated-video" preload="metadata"></video>
|
||
</div>
|
||
<p class="media-caption">🎥 "${escapeHtml(media.originalPrompt)}"</p>
|
||
<div class="media-actions-row">
|
||
<a href="${media.url}" download="${getDownloadFileName('video', 'mp4')}" target="_blank" class="media-download-btn">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||
</svg>
|
||
<span>Salvar Vídeo</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
} else {
|
||
if (content.trim().startsWith('<div class="media-message-card">')) {
|
||
contentHtml = content;
|
||
} else {
|
||
contentHtml = marked.parse(content);
|
||
}
|
||
}
|
||
}
|
||
|
||
const actionsHtml = !isUser ? `
|
||
<div class="message-actions">
|
||
<button class="msg-action-btn btn-copy-msg" title="Copiar resposta" data-content="${media ? escapeHtml(content) : escapeHtml(content).replace(/"/g, '"')}">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
|
||
</svg>
|
||
<span>Copiar</span>
|
||
</button>
|
||
<button class="msg-action-btn btn-regenerate" title="Regenerar resposta">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||
</svg>
|
||
<span>Regenerar</span>
|
||
</button>
|
||
<button class="msg-action-btn btn-speak" title="Ouvir resposta" data-content="${media ? escapeHtml(content) : escapeHtml(content).replace(/"/g, '"')}">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.114 5.636a9 9 0 0 1 0 12.728M16.463 8.288a5.25 5.25 0 0 1 0 7.424M6.75 8.25l4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z" />
|
||
</svg>
|
||
<span>Ouvir</span>
|
||
</button>
|
||
</div>
|
||
` : `
|
||
<div class="message-actions">
|
||
<button class="msg-action-btn btn-edit" title="Editar e reenviar" data-content="${escapeHtml(content).replace(/"/g, '"')}">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
||
</svg>
|
||
<span>Editar</span>
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
row.innerHTML = `
|
||
<div class="message-icon">${avatarHtml}</div>
|
||
<div class="message-content-wrapper">
|
||
<div class="message-content">${contentHtml}</div>
|
||
${actionsHtml}
|
||
</div>
|
||
`;
|
||
|
||
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 = `
|
||
<div class="message-icon">
|
||
<div class="avatar bot-avatar bot-avatar-img-wrapper"><img src="${window.agentConfig?.iaAvatarUrl || window.agentConfig?.kemilyAvatarUrl || 'assets/pedagog_ia.png'}" alt="${window.agentConfig?.agentName || 'PedagogIA'}" class="bot-avatar-img"></div>
|
||
</div>
|
||
<div class="message-content-wrapper">
|
||
<div class="message-content">
|
||
<span class="typing-cursor"></span>
|
||
</div>
|
||
</div>
|
||
`;
|
||
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 = `
|
||
<div class="ia-progress-container" id="iaProgressContainer">
|
||
<div class="ia-progress-header">
|
||
<div class="ia-progress-icon"></div>
|
||
<div class="ia-progress-step-text" id="iaProgressStepText">${progressSteps[0].text}</div>
|
||
</div>
|
||
<div class="ia-progress-bar">
|
||
<div class="ia-progress-bar-fill" id="iaProgressBarFill" style="width: ${progressSteps[0].progress}%;"></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
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 = `
|
||
<div class="media-loading">
|
||
<div class="media-spinner"></div>
|
||
<p class="media-loading-text">${escapeHtml(parsed.message)}</p>
|
||
</div>
|
||
`;
|
||
scrollToBottom();
|
||
} else if (parsed.type === 'image') {
|
||
isMediaResponse = true;
|
||
if (progressTimer) {
|
||
clearInterval(progressTimer);
|
||
progressTimer = null;
|
||
}
|
||
mediaMsgData = { type: 'image', url: parsed.url, originalPrompt: parsed.originalPrompt };
|
||
assistantContent = `
|
||
<div class="media-message-card">
|
||
<div class="media-img-container">
|
||
<img src="${parsed.url}" alt="Ilustração gerada" class="generated-image" onload="scrollToBottom()">
|
||
</div>
|
||
<p class="media-caption">✨ "${escapeHtml(parsed.originalPrompt)}"</p>
|
||
<div class="media-actions-row">
|
||
<a href="${parsed.url}" download="${getDownloadFileName('imagem', 'png')}" target="_blank" class="media-download-btn">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||
</svg>
|
||
<span>Salvar na Galeria</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
`;
|
||
botContentDiv.innerHTML = assistantContent;
|
||
scrollToBottom();
|
||
} else if (parsed.type === 'error') {
|
||
isMediaResponse = true;
|
||
if (progressTimer) {
|
||
clearInterval(progressTimer);
|
||
progressTimer = null;
|
||
}
|
||
botContentDiv.innerHTML = `
|
||
<div class="message-row bot">
|
||
<div class="message-content">
|
||
<p style="color: red; font-weight: bold;">⚠️ Erro na geração: ${escapeHtml(parsed.message)}</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
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 = `
|
||
<div class="media-message-card">
|
||
<div class="custom-audio-player">
|
||
<audio src="${parsed.url}" id="${audioId}"></audio>
|
||
<button class="audio-play-btn" onclick="toggleCustomAudio(this)" title="Ouvir música">
|
||
<svg class="play-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M8 5v14l11-7z"/>
|
||
</svg>
|
||
<svg class="pause-icon hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
||
</svg>
|
||
</button>
|
||
<div class="audio-timeline-container">
|
||
<div class="audio-timeline">
|
||
<div class="audio-progress"></div>
|
||
</div>
|
||
</div>
|
||
<span class="audio-time">0:00</span>
|
||
</div>
|
||
<p class="media-caption">🎵 "${escapeHtml(parsed.originalPrompt)}"</p>
|
||
<div class="media-actions-row">
|
||
<a href="${parsed.url}" download="${getDownloadFileName('musica', 'mp3')}" target="_blank" class="media-download-btn">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||
</svg>
|
||
<span>Baixar Música</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
`;
|
||
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 = `
|
||
<div class="media-message-card">
|
||
<div class="media-video-container">
|
||
<video controls src="${parsed.url}" class="generated-video" preload="metadata"></video>
|
||
</div>
|
||
<p class="media-caption">🎥 "${escapeHtml(parsed.originalPrompt)}"</p>
|
||
<div class="media-actions-row">
|
||
<a href="${parsed.url}" download="${getDownloadFileName('video', 'mp4')}" target="_blank" class="media-download-btn">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||
</svg>
|
||
<span>Salvar Vídeo</span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
`;
|
||
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 <think> nativos de modelos de reasoning antes de renderizar e extrair conhecimento
|
||
assistantContent = assistantContent.replace(/<think>[\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 = `
|
||
<button class="msg-action-btn btn-copy-msg" title="Copiar resposta" data-content="${escapedContent}">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
|
||
</svg>
|
||
<span>Copiar</span>
|
||
</button>
|
||
<button class="msg-action-btn btn-regenerate" title="Regenerar resposta">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||
</svg>
|
||
<span>Regenerar</span>
|
||
</button>
|
||
<button class="msg-action-btn btn-speak" title="Ouvir resposta" data-content="${escapedContent}">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.114 5.636a9 9 0 0 1 0 12.728M16.463 8.288a5.25 5.25 0 0 1 0 7.424M6.75 8.25l4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z" />
|
||
</svg>
|
||
<span>Ouvir</span>
|
||
</button>
|
||
`;
|
||
|
||
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 = `
|
||
<p style="color: var(--error); margin-top: 8px; font-size: 13.5px; display: flex; align-items: center; gap: 6px;">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 16px; height: 16px;">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
|
||
</svg>
|
||
Desculpe, ocorreu um erro ao obter resposta da PedaGog. Por favor, tente novamente.
|
||
</p>
|
||
`;
|
||
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
|
||
? `<div class="report-meta-item"><span class="meta-label">👧 Crianças:</span> ${data.criancas.join(', ')}</div>`
|
||
: `<div class="report-meta-item"><span class="meta-label">👧 Crianças:</span> <em>não identificadas</em></div>`;
|
||
const tagsHtml = data.tags && data.tags.length
|
||
? `<div class="report-meta-item"><span class="meta-label">🏷️ Tags:</span> ${data.tags.map(t => `<span class="tag-chip">${t}</span>`).join(' ')}</div>`
|
||
: '';
|
||
const turmaHtml = data.turma && data.turma !== 'não informada'
|
||
? `<div class="report-meta-item"><span class="meta-label">🏫 Turma:</span> ${data.turma}</div>`
|
||
: '';
|
||
|
||
reportContent.innerHTML = `
|
||
<div class="report-meta">${criancasHtml}${turmaHtml}${tagsHtml}</div>
|
||
<hr style="border-color: var(--border-light); margin: 12px 0;">
|
||
${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);
|
||
}
|
||
|
||
// Fechar clicando fora
|
||
if (recordModal) {
|
||
recordModal.addEventListener('click', (e) => {
|
||
if (e.target === recordModal) 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 = '<div class="obs-empty">Carregando...</div>';
|
||
|
||
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 = '<div class="obs-empty">Erro ao carregar observações.</div>';
|
||
}
|
||
});
|
||
}
|
||
|
||
// 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 = '<div class="obs-empty">Carregando...</div>';
|
||
|
||
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 = '<div class="obs-empty">Erro ao carregar observações.</div>';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Fechar modal
|
||
if (btnCloseObsModal) {
|
||
btnCloseObsModal.addEventListener('click', () => {
|
||
obsModal.style.display = 'none';
|
||
});
|
||
}
|
||
if (obsModal) {
|
||
obsModal.addEventListener('click', (e) => {
|
||
if (e.target === obsModal) 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 = '<option value="">Todos os meses</option>' +
|
||
months.map(m => `<option value="${m}">${m.split('-')[1]}/${m.split('-')[0]}</option>`).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 = '<div class="obs-empty">Nenhuma criança com "Educação Especial" cadastrada na Minha Turma.</div>';
|
||
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 = '<div class="obs-empty">Nenhuma observação encontrada.</div>';
|
||
return;
|
||
}
|
||
|
||
obsList.innerHTML = filtered.map(o => {
|
||
const tagsHtml = (o.tags || []).map(t => `<span class="obs-tag">${t}</span>`).join('');
|
||
const criancasHtml = (o.criancas || []).map(c => `<span class="obs-crianca">${c}</span>`).join('');
|
||
const dateFormatted = o.date ? o.date.split('-').reverse().join('/') : '';
|
||
return `
|
||
<div class="obs-item" data-filename="${o.filename}" data-yearmonth="${o.date?.slice(0, 7)}">
|
||
<div class="obs-item-header">
|
||
<strong>📋 ${dateFormatted}${o.time ? ' às ' + o.time : ''}</strong>
|
||
${o.turma && o.turma !== 'não informada' ? `<span class="obs-crianca">🏫 ${o.turma}</span>` : ''}
|
||
</div>
|
||
<div class="obs-item-preview">${o.preview || 'Sem prévia disponível.'}</div>
|
||
<div class="obs-item-footer">${tagsHtml}${criancasHtml}</div>
|
||
</div>
|
||
`;
|
||
}).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 = '<option value="">Todas as Turmas</option>' + currentTurmas.map(t => `<option value="${t.id}">${t.nome}</option>`).join('');
|
||
if(filterAlunosTurma) filterAlunosTurma.innerHTML = opts;
|
||
const formOpts = '<option value="">Selecione uma turma...</option>' + currentTurmas.map(t => `<option value="${t.id}">${t.nome}</option>`).join('');
|
||
if(cFormTurma) cFormTurma.innerHTML = formOpts;
|
||
if(document.getElementById('emitirFilterTurma')) {
|
||
document.getElementById('emitirFilterTurma').innerHTML = '<option value="">Todas as turmas</option>' + currentTurmas.map(t => `<option value="${t.id}">${t.nome}</option>`).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 = '<div style="color: var(--text-secondary); text-align: center; padding: 20px;">Nenhuma turma cadastrada.</div>';
|
||
return;
|
||
}
|
||
turmasList.innerHTML = currentTurmas.map(t => `
|
||
<div style="background: var(--bg-secondary); border: 1px solid var(--border-light); padding: 12px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center;">
|
||
<div>
|
||
<div style="font-weight: 600; color: var(--text-primary);">${escapeHtml(t.nome)}</div>
|
||
<div style="font-size: 0.8rem; color: var(--text-secondary);">${t.periodo || ''} ${t.sala ? '- ' + t.sala : ''}</div>
|
||
</div>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button onclick="editTurma('${t.id}')" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 4px;" title="Editar">✏️</button>
|
||
<button onclick="deleteTurma('${t.id}')" style="background: none; border: none; color: #ef4444; cursor: pointer; padding: 4px;" title="Excluir">🗑️</button>
|
||
</div>
|
||
</div>
|
||
`).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 = '<div style="color: var(--text-secondary); text-align: center; padding: 20px;">Nenhuma criança encontrada.</div>';
|
||
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 `
|
||
<div style="background: var(--bg-secondary); border: 1px solid var(--border-light); padding: 12px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px;">
|
||
<div>
|
||
<div style="font-weight: 600; color: var(--text-primary);">${escapeHtml(c.nome)} ${c.especial ? '⭐' : ''}</div>
|
||
<div style="font-size: 0.8rem; color: var(--text-secondary);">${escapeHtml(turmaNome)} ${c.apelido ? '- ' + escapeHtml(c.apelido) : ''}</div>
|
||
</div>
|
||
<div style="display: flex; gap: 6px; align-items: center;">
|
||
<button onclick="viewChildTimeline('${c.id}')" style="background: rgba(59,130,246,0.12); border: 1px solid rgba(59,130,246,0.3); color: #60a5fa; cursor: pointer; padding: 4px 8px; border-radius: 6px; font-size: 0.75rem; font-weight: 600;" title="Ver Linha do Tempo e Dossiê">🔍 Dossiê</button>
|
||
<button onclick="editChild('${c.id}')" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 4px;" title="Editar">✏️</button>
|
||
<button onclick="deleteChild('${c.id}')" style="background: none; border: none; color: #ef4444; cursor: pointer; padding: 4px;" title="Excluir">🗑️</button>
|
||
</div>
|
||
</div>
|
||
`}).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 (minhaTurmaModal) {
|
||
minhaTurmaModal.addEventListener('click', (e) => {
|
||
if (e.target === minhaTurmaModal) 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 = `
|
||
<div class="settings-modal-content obs-modal-content" style="max-width: 680px;">
|
||
<div class="settings-modal-header">
|
||
<h3>📋 Observação</h3>
|
||
<button class="btn-close-modal" onclick="this.closest('.settings-modal').remove()">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div style="padding: 20px; overflow-y: auto; max-height: 70vh;">
|
||
<div class="obs-detail-content" style="color: var(--text-primary); line-height: 1.6;">
|
||
${marked.parse(mdText.replace(/^---[\s\S]*?---\n/, ''))}
|
||
</div>
|
||
<div style="margin-top: 16px; display: flex; gap: 8px;">
|
||
<button class="btn-save-report" onclick="navigator.clipboard.writeText(this.closest('.settings-modal').querySelector('.obs-detail-content').innerText).then(()=>alert('Copiado!'))">Copiar Texto</button>
|
||
<button class="btn-discard" onclick="this.closest('.settings-modal').remove()">Fechar</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
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';
|
||
});
|
||
}
|
||
if (modelosRelatorioModal) {
|
||
modelosRelatorioModal.addEventListener('click', (e) => {
|
||
if (e.target === modelosRelatorioModal) modelosRelatorioModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
// Buscar modelos do backend
|
||
async function fetchTemplates() {
|
||
templatesList.innerHTML = '<div style="color: var(--text-secondary); text-align: center; padding: 20px;">Carregando...</div>';
|
||
try {
|
||
const res = await fetch('/api/modelos');
|
||
allTemplates = await res.json();
|
||
renderTemplatesList();
|
||
} catch (err) {
|
||
console.error('Erro ao buscar modelos:', err);
|
||
templatesList.innerHTML = '<div style="color: #ff6b6b; text-align: center; padding: 20px;">Erro ao carregar modelos.</div>';
|
||
}
|
||
}
|
||
|
||
// Renderizar a lista de modelos na esquerda
|
||
function renderTemplatesList() {
|
||
if (allTemplates.length === 0) {
|
||
templatesList.innerHTML = '<div style="color: var(--text-secondary); text-align: center; padding: 20px;">Nenhum modelo cadastrado.</div>';
|
||
return;
|
||
}
|
||
templatesList.innerHTML = allTemplates.map(t => `
|
||
<div class="obs-item" style="padding: 10px; margin: 0 0 8px 0; cursor: pointer; border-left: 3px solid var(--brand-green); display: flex; flex-direction: column; gap: 4px; background: var(--bg-secondary); border-radius: 8px;" data-id="${t.id}">
|
||
<div style="font-weight: 600; color: var(--text-primary); font-size: 0.9rem;">${t.nome}</div>
|
||
<div style="font-size: 0.75rem; color: var(--text-secondary);">${t.periodicidade} • ${t.finalidade || 'Sem finalidade'}</div>
|
||
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px;">
|
||
<button class="template-duplicate-btn" data-id="${t.id}" style="background: none; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 2px 8px; border-radius: 4px; font-size: 0.72rem; cursor: pointer; transition: opacity 0.2s;">Duplicar</button>
|
||
<button class="template-edit-btn" data-id="${t.id}" style="background: none; border: 1px solid var(--border-light); color: var(--brand-green); padding: 2px 8px; border-radius: 4px; font-size: 0.72rem; cursor: pointer;">Editar</button>
|
||
<button class="template-delete-btn" data-id="${t.id}" style="background: none; border: 1px solid var(--border-light); color: #ff6b6b; padding: 2px 8px; border-radius: 4px; font-size: 0.72rem; cursor: pointer;">Excluir</button>
|
||
</div>
|
||
</div>
|
||
`).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 = `
|
||
<div style="color: var(--text-secondary); text-align: center; margin-top: 50px;">
|
||
Escolha uma criança e um modelo de relatório para começar.
|
||
</div>
|
||
`;
|
||
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';
|
||
});
|
||
}
|
||
if (emitirRelatorioModal) {
|
||
emitirRelatorioModal.addEventListener('click', (e) => {
|
||
if (e.target === emitirRelatorioModal) 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 = '<option value="">Selecione a criança</option>' +
|
||
`<option value="todas" ${activeCrianca === 'todas' ? 'selected' : ''}>Todas as crianças (Relatório Global)</option>` +
|
||
(Array.isArray(criancas) ? criancas : []).map(c => `<option value="${c}" ${c === activeCrianca ? 'selected' : ''}>${c}</option>`).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 = '<option value="">Todos os anos</option>' +
|
||
(meta.anos || []).map(y => `<option value="${y}" ${y === activeAno ? 'selected' : ''}>${y}</option>`).join('');
|
||
}
|
||
if (emitirFilterTurma) {
|
||
emitirFilterTurma.innerHTML = '<option value="">Todas as turmas</option>' +
|
||
(meta.turmas || []).map(t => `<option value="${t}" ${t === activeTurma ? 'selected' : ''}>${t}</option>`).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 = '<option value="">Selecione o modelo</option>' +
|
||
(Array.isArray(modelos) ? modelos : []).map(m => `<option value="${m.id}" ${m.id == activeTemplate ? 'selected' : ''}>${m.nome}</option>`).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 = `⚠️ <span style="color: #ff6b6b; font-weight: 600;">Nenhuma observação</span> encontrada para esses filtros.`;
|
||
btnGenerateCompiledReport.disabled = true;
|
||
btnGenerateCompiledReport.style.opacity = '0.6';
|
||
btnGenerateCompiledReport.style.cursor = 'not-allowed';
|
||
} else {
|
||
emitirObsCountInfo.innerHTML = `✨ <strong>${count}</strong> 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 = `
|
||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 12px; margin-top: 50px;">
|
||
<div style="width: 32px; height: 32px; border: 3px solid var(--border-light); border-top-color: var(--brand-green); border-radius: 50%; animation: spin 1s linear infinite;"></div>
|
||
<div style="color: var(--text-secondary); font-size: 0.9rem; font-weight: 500;">Analisando observações e gerando relatório pedagógico...</div>
|
||
</div>
|
||
`;
|
||
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 = `
|
||
<div style="background: var(--bg-primary); padding: 12px; border-radius: 8px; margin-bottom: 16px; border: 1px solid var(--border-light); font-size: 0.85rem;">
|
||
<div><strong>Estudante:</strong> ${data.crianca}</div>
|
||
<div><strong>Período:</strong> ${data.periodo}</div>
|
||
<div><strong>Modelo Aplicado:</strong> ${data.templateNome}</div>
|
||
<div><strong>Quantidade de Observações Consolidadas:</strong> ${data.totalObservacoes}</div>
|
||
</div>
|
||
<div class="report-content-body" style="color: var(--text-primary);">
|
||
${marked.parse(data.report)}
|
||
</div>
|
||
`;
|
||
|
||
// 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 = `
|
||
<div style="color: #ff6b6b; text-align: center; margin-top: 50px; font-weight: 500;">
|
||
Erro ao gerar relatório: ${err.message}
|
||
</div>
|
||
`;
|
||
} finally {
|
||
btnGenerateCompiledReport.disabled = false;
|
||
btnGenerateCompiledReport.style.opacity = '1';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Exportar para DOCX (Formato HTML-DOCX)
|
||
if (btnExportDocx) {
|
||
btnExportDocx.addEventListener('click', () => {
|
||
if (!activeCompiledReport) return;
|
||
|
||
const htmlContent = `
|
||
<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:office:word" xmlns="http://www.w3.org/TR/REC-html40">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Relatório Pedagógico - ${activeCompiledReport.crianca}</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; line-height: 1.5; color: #333; }
|
||
h1, h2, h3 { color: #1f2937; }
|
||
.header-info { background-color: #f3f4f6; padding: 12px; border-radius: 6px; margin-bottom: 20px; border: 1px solid #e5e7eb; }
|
||
.header-info div { margin-bottom: 4px; font-size: 11pt; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header-info">
|
||
<div><b>Estudante:</b> ${activeCompiledReport.crianca}</div>
|
||
<div><b>Período:</b> ${activeCompiledReport.periodo}</div>
|
||
<div><b>Modelo:</b> ${activeCompiledReport.templateNome}</div>
|
||
<div><b>Quantidade de observações consolidadas:</b> ${activeCompiledReport.totalObservacoes}</div>
|
||
</div>
|
||
<hr/>
|
||
${marked.parse(activeCompiledReport.report)}
|
||
</body>
|
||
</html>
|
||
`;
|
||
|
||
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(`
|
||
<html>
|
||
<head>
|
||
<title>Relatório Pedagógico - ${activeCompiledReport.crianca}</title>
|
||
<style>
|
||
body { font-family: 'Outfit', 'Inter', -apple-system, sans-serif; padding: 40px; color: #333; line-height: 1.6; }
|
||
h1, h2, h3 { color: #1e293b; margin-top: 24px; margin-bottom: 12px; }
|
||
h1 { border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; font-size: 22px; }
|
||
h2 { font-size: 18px; border-bottom: 1px solid #f1f5f9; padding-bottom: 6px; }
|
||
.meta-box { background: #f8fafc; padding: 16px; border-radius: 8px; margin-bottom: 24px; font-size: 14px; border: 1px solid #e2e8f0; }
|
||
.meta-item { margin-bottom: 6px; display: flex; }
|
||
.meta-label { font-weight: bold; color: #475569; width: 220px; }
|
||
p { margin-bottom: 14px; text-align: justify; }
|
||
ul, ol { margin-bottom: 14px; padding-left: 20px; }
|
||
li { margin-bottom: 6px; }
|
||
@media print {
|
||
body { padding: 0; }
|
||
button { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>Relatório Pedagógico de Desenvolvimento</h1>
|
||
<div class="meta-box">
|
||
<div class="meta-item"><span class="meta-label">Estudante:</span> <span>${activeCompiledReport.crianca}</span></div>
|
||
<div class="meta-item"><span class="meta-label">Período:</span> <span>${activeCompiledReport.periodo}</span></div>
|
||
<div class="meta-item"><span class="meta-label">Modelo Aplicado:</span> <span>${activeCompiledReport.templateNome}</span></div>
|
||
<div class="meta-item"><span class="meta-label">Quantidade de Observações:</span> <span>${activeCompiledReport.totalObservacoes}</span></div>
|
||
</div>
|
||
<div>${marked.parse(activeCompiledReport.report)}</div>
|
||
<script>
|
||
window.onload = function() {
|
||
window.print();
|
||
setTimeout(() => { window.close(); }, 500);
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|
||
`);
|
||
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();
|
||
});
|
||
}
|
||
|
||
// Fechar modal clicando fora
|
||
window.addEventListener('click', (e) => {
|
||
if (e.target === estudioMusicalModal) {
|
||
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 = '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">Carregando histórico...</div>';
|
||
try {
|
||
const res = await fetch('/api/music/list');
|
||
const data = await res.json();
|
||
estudioHistoryList.innerHTML = '';
|
||
if (data.length === 0) {
|
||
estudioHistoryList.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">Nenhuma canção salva ainda no estúdio.</div>';
|
||
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 = `
|
||
<div style="flex: 1; margin-right: 15px;">
|
||
<div style="font-weight: 600; font-size: 0.95rem; color: var(--text-primary);">🎵 ${item.tema || 'Música'}</div>
|
||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-top: 4px;">Voz: ${item.voz} | Ritmo: ${item.ritmo}</div>
|
||
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 4px;">${new Date(item.created_at).toLocaleString()}</div>
|
||
</div>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button class="btn-ver-estudio btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.85rem; border-radius: 6px;">Ver/Ouvir</button>
|
||
<button class="btn-del-estudio btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.85rem; border-radius: 6px; color: #ef4444; border-color: rgba(239, 68, 68, 0.3);">🗑️</button>
|
||
</div>
|
||
`;
|
||
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 = '<div style="color: #ef4444; padding: 20px;">Erro ao carregar histórico.</div>';
|
||
}
|
||
});
|
||
}
|
||
|
||
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) throw new Error('Falha ao listar projetos');
|
||
const projects = await resp.json();
|
||
|
||
comicsProjectSelect.innerHTML = '<option value="">-- Carregar projeto salvo... --</option>';
|
||
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.error('Erro ao listar projetos:', err);
|
||
}
|
||
};
|
||
|
||
const loadComicsProject = async (id) => {
|
||
if (!id) {
|
||
startNewComicsProject();
|
||
return;
|
||
}
|
||
try {
|
||
const resp = await fetch(`/api/comics/projects/${id}`);
|
||
if (!resp.ok) throw new Error('Falha ao carregar projeto');
|
||
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) {
|
||
comicsResultTitle.textContent = project.titulo || 'História em Quadrinhos';
|
||
comicsGridContainer.innerHTML = '';
|
||
|
||
generatedPanelsData.forEach((panel, index) => {
|
||
const panelCard = document.createElement('div');
|
||
panelCard.className = 'comic-panel-card';
|
||
|
||
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;
|
||
img.alt = `Quadro ${panel.panel_number}`;
|
||
imgContainer.appendChild(img);
|
||
|
||
const badge = document.createElement('div');
|
||
badge.className = 'comic-panel-number-badge';
|
||
badge.textContent = panel.panel_number;
|
||
imgContainer.appendChild(badge);
|
||
|
||
panelCard.appendChild(imgContainer);
|
||
|
||
const caption = document.createElement('div');
|
||
caption.className = 'comic-caption-text';
|
||
caption.style.cssText = 'padding: 10px 14px; background: var(--bg-primary); border-top: 1px solid var(--border-light);';
|
||
|
||
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: 50px;';
|
||
|
||
textarea.addEventListener('input', (e) => {
|
||
panel.dialogue = e.target.value;
|
||
if (generatedPanelsData[index]) {
|
||
generatedPanelsData[index].dialogue = e.target.value;
|
||
}
|
||
});
|
||
|
||
caption.appendChild(textarea);
|
||
panelCard.appendChild(caption);
|
||
|
||
comicsGridContainer.appendChild(panelCard);
|
||
});
|
||
|
||
comicsResultArea.style.display = 'flex';
|
||
} else {
|
||
comicsResultArea.style.display = 'none';
|
||
}
|
||
|
||
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) => {
|
||
const titulo = comicsProjectTitleInput ? comicsProjectTitleInput.value.trim() : '';
|
||
if (!titulo) {
|
||
await showCustomAlert('Campo Obrigatório', 'Por favor, digite um título para salvar seu projeto.');
|
||
return;
|
||
}
|
||
|
||
const characters = [];
|
||
document.querySelectorAll('.comic-char-row').forEach(row => {
|
||
const typeInput = row.querySelector('.char-type-input');
|
||
const nameInput = row.querySelector('.char-name-input');
|
||
if (typeInput && nameInput) {
|
||
const type = typeInput.value.trim();
|
||
const name = nameInput.value.trim();
|
||
const isAnimal = row.querySelector('span').textContent === '🐾';
|
||
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 body = {
|
||
id: targetId,
|
||
titulo,
|
||
tema: comicsTemaInput.value.trim(),
|
||
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 err = await resp.json();
|
||
throw new Error(err.error || 'Erro ao salvar projeto');
|
||
}
|
||
|
||
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);
|
||
}
|
||
};
|
||
|
||
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) throw new Error('Falha ao excluir projeto');
|
||
|
||
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', () => {
|
||
fabricaQuadrinhosModal.style.display = 'none';
|
||
if (comicsVideoPlayer) comicsVideoPlayer.pause();
|
||
});
|
||
}
|
||
|
||
// Fechar modal clicando fora
|
||
window.addEventListener('click', (e) => {
|
||
if (e.target === fabricaQuadrinhosModal) {
|
||
fabricaQuadrinhosModal.style.display = 'none';
|
||
if (comicsVideoPlayer) comicsVideoPlayer.pause();
|
||
}
|
||
});
|
||
|
||
// Helper para converter URL de imagem local em base64
|
||
const getBase64Image = async (imgUrl) => {
|
||
return new Promise((resolve, reject) => {
|
||
const img = new Image();
|
||
img.crossOrigin = 'Anonymous';
|
||
img.onload = () => {
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = img.width;
|
||
canvas.height = img.height;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.drawImage(img, 0, 0);
|
||
resolve(canvas.toDataURL('image/jpeg'));
|
||
};
|
||
img.onerror = (e) => reject(new Error('Falha ao carregar imagem para o PDF: ' + imgUrl));
|
||
img.src = imgUrl;
|
||
});
|
||
};
|
||
|
||
// 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 err = await scriptResp.json();
|
||
throw new Error(err.error || 'Erro ao planejar roteiro');
|
||
}
|
||
|
||
const scriptData = await scriptResp.json();
|
||
comicsLoaderProgress.style.width = '15%';
|
||
|
||
if (scriptData.character_description) {
|
||
currentComicsCharDescEnglish = scriptData.character_description;
|
||
}
|
||
|
||
// 2. Gerar quadrinhos em PARALELO com tracking de progresso em tempo real
|
||
if (genMode !== 'extend') {
|
||
generatedPanelsData = [];
|
||
}
|
||
const totalPanels = scriptData.panels.length;
|
||
let completedCount = 0;
|
||
|
||
const panelPromises = scriptData.panels.map(async (panel, idx) => {
|
||
// Injetamos a proporção e estilo de forma reforçada no prompt
|
||
const fullPrompt = `${panel.image_prompt}, high resolution children book illustration, cute Pixar style, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`;
|
||
|
||
const frameResp = await fetch('/api/comics/generate-frame', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ prompt: fullPrompt })
|
||
});
|
||
|
||
if (!frameResp.ok) {
|
||
const err = await frameResp.json();
|
||
throw new Error(`Erro no quadrinho ${panel.panel_number}: ${err.error}`);
|
||
}
|
||
|
||
const frameData = await frameResp.json();
|
||
completedCount++;
|
||
const progressPercent = 15 + Math.floor((completedCount / totalPanels) * 80);
|
||
if (comicsLoaderProgress) comicsLoaderProgress.style.width = `${progressPercent}%`;
|
||
if (comicsLoaderText) comicsLoaderText.textContent = `⚡ Ilustrando em paralelo: ${completedCount}/${totalPanels} quadros concluídos...`;
|
||
|
||
return {
|
||
panel_number: panel.panel_number,
|
||
imageUrl: frameData.imageUrl,
|
||
dialogue: panel.dialogue || '',
|
||
image_prompt: panel.image_prompt || ''
|
||
};
|
||
});
|
||
|
||
const newPanels = await Promise.all(panelPromises);
|
||
|
||
// 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';
|
||
const metaPanels = document.getElementById('comicsMetaPanels');
|
||
if (metaPanels) metaPanels.textContent = `🖼️ ${generatedPanelsData.length} Quadros`;
|
||
const metaRatio = document.getElementById('comicsMetaRatio');
|
||
if (metaRatio) metaRatio.textContent = `📐 ${selectedComicsRatio} ${selectedComicsRatio === '16:9' ? 'HD' : 'Retrô'}`;
|
||
|
||
// 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;
|
||
|
||
} 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
|
||
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';
|
||
|
||
const promptToUse = panel.image_prompt || 'cute children illustration';
|
||
const fullPrompt = `${promptToUse}, high resolution children book illustration, cute Pixar style, ${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 })
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
const err = await resp.json();
|
||
throw new Error(err.error || 'Erro na regeneração');
|
||
}
|
||
|
||
const data = await resp.json();
|
||
panel.imageUrl = data.imageUrl;
|
||
imgElement.src = data.imageUrl + '?v=' + Date.now();
|
||
imgElement.style.opacity = '1';
|
||
} 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 = '<span>📦 Baixar Todas as Imagens (ZIP)</span>';
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- 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, 50, { align: 'center' });
|
||
|
||
// Imagem
|
||
const base64Img = await getBase64Image(panel.imageUrl);
|
||
const imgWidth = 515;
|
||
const imgHeight = selectedComicsRatio === '16:9' ? 290 : 386;
|
||
doc.addImage(base64Img, 'JPEG', 40, 80, imgWidth, imgHeight);
|
||
|
||
// Altura esticada para a caixa de diálogo (160pt de altura - aprox dobro)
|
||
const textHeight = 160;
|
||
const textY = 80 + imgHeight;
|
||
|
||
// Caixa de diálogo com fundo cinza claro e borda integrada
|
||
doc.setFillColor(248, 250, 252);
|
||
doc.rect(40, textY, imgWidth, textHeight, 'F');
|
||
|
||
// Moldura unificada ao redor de toda a imagem + texto
|
||
doc.setDrawColor(30, 41, 59);
|
||
doc.setLineWidth(2);
|
||
doc.rect(40, 80, imgWidth, imgHeight + textHeight, 'D');
|
||
|
||
// Linha divisória entre a imagem e a caixa de texto
|
||
doc.line(40, textY, 555, textY);
|
||
|
||
// Texto maior e mais legível (dobro do tamanho relativo - 15pt)
|
||
if (panel.dialogue) {
|
||
doc.setFont('helvetica', 'normal');
|
||
doc.setFontSize(15);
|
||
doc.setTextColor(15, 23, 42);
|
||
|
||
const splitText = doc.splitTextToSize(panel.dialogue, imgWidth - 40);
|
||
doc.text(splitText, 60, textY + 35);
|
||
}
|
||
|
||
// Círculo discreto com o número do quadrinho no canto superior esquerdo da imagem
|
||
doc.setFillColor(254, 240, 138);
|
||
doc.circle(65, 105, 12, 'F');
|
||
doc.setDrawColor(30, 41, 59);
|
||
doc.setLineWidth(1.5);
|
||
doc.circle(65, 105, 12, 'D');
|
||
|
||
doc.setFont('helvetica', 'bold');
|
||
doc.setFontSize(10);
|
||
doc.setTextColor(30, 41, 59);
|
||
doc.text(String(panel.panel_number), 65, 109, { 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 da página paisagem
|
||
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 cardWidth = 376;
|
||
|
||
// Proporções exatas mantidas
|
||
const imgHeight = selectedComicsRatio === '16:9' ? 211 : 282;
|
||
const textHeight = 140; // Dobro de tamanho (antes era 65)
|
||
|
||
// Imagem do quadrinho
|
||
const base64Img = await getBase64Image(panel.imageUrl);
|
||
doc.addImage(base64Img, 'JPEG', startX, 70, cardWidth, imgHeight);
|
||
|
||
// Fundo da caixa de texto
|
||
const textY = 70 + imgHeight;
|
||
doc.setFillColor(248, 250, 252);
|
||
doc.rect(startX, textY, cardWidth, textHeight, 'F');
|
||
|
||
// Moldura unificada ao redor do quadro (imagem + texto)
|
||
doc.setDrawColor(30, 41, 59);
|
||
doc.setLineWidth(2);
|
||
doc.rect(startX, 70, cardWidth, imgHeight + textHeight, 'D');
|
||
|
||
// Linha separando a imagem da caixa de texto
|
||
doc.line(startX, textY, startX + cardWidth, textY);
|
||
|
||
// Legenda formatada em tamanho maior (13pt)
|
||
if (panel.dialogue) {
|
||
doc.setFont('helvetica', 'normal');
|
||
doc.setFontSize(13);
|
||
doc.setTextColor(15, 23, 42);
|
||
|
||
const splitText = doc.splitTextToSize(panel.dialogue, cardWidth - 30);
|
||
doc.text(splitText, startX + 15, textY + 30);
|
||
}
|
||
|
||
// Círculo discreto com o numeral do quadro no canto superior esquerdo da imagem
|
||
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)
|
||
if (btnCompileComicsVideo) {
|
||
btnCompileComicsVideo.addEventListener('click', async () => {
|
||
if (generatedPanelsData.length === 0) return;
|
||
|
||
btnCompileComicsVideo.disabled = true;
|
||
comicsVideoLoader.style.display = 'flex';
|
||
comicsVideoResult.style.display = 'none';
|
||
|
||
try {
|
||
const response = await fetch('/api/comics/generate-video', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
frames: generatedPanelsData,
|
||
musicSelection: comicsVideoMusic.value,
|
||
frameDuration: comicsVideoDuration.value
|
||
})
|
||
});
|
||
|
||
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;
|
||
}
|
||
});
|
||
}
|
||
|
||
// ============================================================
|
||
// 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';
|
||
});
|
||
}
|
||
|
||
window.addEventListener('click', (e) => {
|
||
if (e.target === videoMindModal) {
|
||
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(`
|
||
<html>
|
||
<head>
|
||
<title>VideoMind - Parecer Pedagógico</title>
|
||
<style>
|
||
body { font-family: 'Outfit', 'Inter', -apple-system, sans-serif; padding: 40px; color: #333; line-height: 1.6; }
|
||
h1, h2, h3 { color: #0891b2; margin-top: 24px; margin-bottom: 12px; }
|
||
h1 { border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; font-size: 22px; }
|
||
h2 { font-size: 18px; border-bottom: 1px solid #f1f5f9; padding-bottom: 6px; }
|
||
.meta-box { background: #f8fafc; padding: 16px; border-radius: 8px; margin-bottom: 24px; font-size: 14px; border: 1px solid #e2e8f0; display: flex; gap: 16px; align-items: center; }
|
||
.meta-info { display: flex; flex-direction: column; gap: 4px; }
|
||
.meta-label { font-weight: bold; color: #475569; }
|
||
p { margin-bottom: 14px; text-align: justify; }
|
||
ul, ol { margin-bottom: 14px; padding-left: 20px; }
|
||
li { margin-bottom: 6px; }
|
||
@media print {
|
||
body { padding: 0; }
|
||
button { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>🎬 VideoMind - Análise & Parecer Pedagógico</h1>
|
||
<div class="meta-box">
|
||
<img src="${activeVideoMindReport.metadata.thumbnail}" style="width: 100px; aspect-ratio: 16/9; object-fit: cover; border-radius: 6px;">
|
||
<div class="meta-info">
|
||
<div><span class="meta-label">Vídeo:</span> <span>${activeVideoMindReport.metadata.title}</span></div>
|
||
<div><span class="meta-label">Canal:</span> <span>${activeVideoMindReport.metadata.author}</span></div>
|
||
<div><span class="meta-label">Resumo Escolhido:</span> <span>${selectedVideoMindLines} Linhas</span></div>
|
||
</div>
|
||
</div>
|
||
<div>${window.marked ? marked.parse(activeVideoMindReport.report) : activeVideoMindReport.report}</div>
|
||
<script>
|
||
window.onload = function() {
|
||
window.print();
|
||
setTimeout(() => { window.close(); }, 500);
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|
||
`);
|
||
});
|
||
}
|
||
|
||
// ============================================================
|
||
// 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';
|
||
});
|
||
}
|
||
|
||
window.addEventListener('click', (e) => {
|
||
if (e.target === garatujasModal) {
|
||
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(`
|
||
<html>
|
||
<head>
|
||
<title>Garatujas - Laudo do Desenho</title>
|
||
<style>
|
||
body { font-family: 'Outfit', 'Inter', sans-serif; padding: 40px; color: #333; line-height: 1.6; }
|
||
h1, h2, h3 { color: #ea580c; margin-top: 24px; margin-bottom: 12px; }
|
||
h1 { border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; font-size: 22px; }
|
||
.meta-box { background: #f8fafc; padding: 16px; border-radius: 8px; margin-bottom: 24px; border: 1px solid #e2e8f0; display: flex; gap: 16px; align-items: center; }
|
||
.meta-label { font-weight: bold; color: #475569; }
|
||
img { border-radius: 6px; border: 1px solid #ddd; max-height: 120px; }
|
||
@media print {
|
||
body { padding: 0; }
|
||
button { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>🎨 Garatujas - Parecer de Traçado Infantil</h1>
|
||
<div class="meta-box">
|
||
<img src="${activeGaratujaReport.imageUrl}">
|
||
<div>
|
||
<div><span class="meta-label">Aluno(a):</span> <span>${activeGaratujaReport.alunoName}</span></div>
|
||
<div><span class="meta-label">Data de Análise:</span> <span>${new Date(activeGaratujaReport.createdAt).toLocaleDateString('pt-BR')}</span></div>
|
||
</div>
|
||
</div>
|
||
<div>${window.marked ? marked.parse(activeGaratujaReport.analise) : activeGaratujaReport.analise}</div>
|
||
<script>
|
||
window.onload = function() {
|
||
window.print();
|
||
setTimeout(() => { window.close(); }, 500);
|
||
};
|
||
<\/script>
|
||
</body>
|
||
</html>
|
||
`);
|
||
printWindow.document.close();
|
||
});
|
||
}
|
||
|
||
// Carregar Histórico Garatujas
|
||
async function loadGaratujasHistory() {
|
||
try {
|
||
const response = await fetch('/api/garatujas/list');
|
||
if (!response.ok) throw new Error('Erro ao carregar histórico');
|
||
const list = await response.json();
|
||
|
||
garatujasListContainer.innerHTML = '';
|
||
if (list.length === 0) {
|
||
garatujasNoData.style.display = 'block';
|
||
return;
|
||
}
|
||
garatujasNoData.style.display = 'none';
|
||
|
||
list.forEach(item => {
|
||
const div = document.createElement('div');
|
||
div.className = 'quick-note-card';
|
||
div.style.cursor = 'pointer';
|
||
div.style.display = 'flex';
|
||
div.style.flexDirection = 'column';
|
||
div.style.gap = '8px';
|
||
div.style.padding = '12px';
|
||
|
||
div.innerHTML = `
|
||
<div style="position: relative; width: 100%; aspect-ratio: 4/3; overflow: hidden; border-radius: 6px; background: rgba(0,0,0,0.2);">
|
||
<img src="${item.imageUrl}" style="width: 100%; height: 100%; object-fit: cover;">
|
||
<button class="delete-btn" data-id="${item.id}" style="position: absolute; top: 6px; right: 6px; background: rgba(0,0,0,0.5); border: none; width: 26px; height: 26px; border-radius: 50%; color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 0.8rem; transition: background 0.2s, transform 0.2s; z-index: 10;" title="Deletar análise" onmouseover="this.style.background='rgba(220,53,69,0.95)'; this.style.transform='scale(1.1)';" onmouseout="this.style.background='rgba(0,0,0,0.5)'; this.style.transform='scale(1)';">
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
<div style="font-weight: 600; color: var(--text-primary); font-size: 0.9rem;">${escapeHtml(item.alunoName)}</div>
|
||
<div style="font-size: 0.75rem; color: var(--text-muted);">${new Date(item.createdAt).toLocaleDateString('pt-BR')}</div>
|
||
`;
|
||
|
||
const deleteBtn = div.querySelector('.delete-btn');
|
||
deleteBtn.addEventListener('click', async (e) => {
|
||
e.stopPropagation();
|
||
if (confirm(`Deseja realmente excluir a análise de desenho do aluno "${item.alunoName}"?`)) {
|
||
try {
|
||
const delResp = await fetch(`/api/garatujas/${item.id}`, { method: 'DELETE' });
|
||
if (!delResp.ok) throw new Error('Falha ao deletar');
|
||
alert('Análise deletada com sucesso.');
|
||
loadGaratujasHistory();
|
||
if (activeGaratujaReport && activeGaratujaReport.id === item.id) {
|
||
activeGaratujaReport = null;
|
||
garatujasAlunoInput.value = '';
|
||
garatujaImgPreview.src = '';
|
||
garatujaPreviewContainer.style.display = 'none';
|
||
garatujaReportArea.innerHTML = '';
|
||
garatujaResultArea.style.display = 'none';
|
||
}
|
||
} catch (err) {
|
||
alert('Erro ao excluir análise: ' + err.message);
|
||
}
|
||
}
|
||
});
|
||
|
||
div.addEventListener('click', async () => {
|
||
try {
|
||
const detailResp = await fetch(`/api/garatujas/${item.id}`);
|
||
if (!detailResp.ok) throw new Error('Erro ao obter detalhes');
|
||
const detail = await detailResp.json();
|
||
|
||
activeGaratujaReport = detail;
|
||
garatujasAlunoInput.value = detail.alunoName;
|
||
garatujaImgPreview.src = detail.imageUrl;
|
||
garatujaPreviewContainer.style.display = 'block';
|
||
garatujaReportArea.innerHTML = window.marked ? marked.parse(detail.analise) : detail.analise;
|
||
|
||
garatujaResultArea.style.display = 'flex';
|
||
if (tabGaratujasNova) tabGaratujasNova.click();
|
||
} catch (e) {
|
||
alert('Falha ao abrir desenho.');
|
||
}
|
||
});
|
||
|
||
garatujasListContainer.appendChild(div);
|
||
});
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// ESTÚDIO DE POESIA
|
||
// ============================================================
|
||
// ESTÚDIO DE POESIAS (ESTÚDIO PROFISSIONAL)
|
||
// ============================================================
|
||
const poesiaModal = document.getElementById('poesiaModal');
|
||
const btnClosePoesiaModal = document.getElementById('btnClosePoesiaModal');
|
||
const barBtnPoesia = document.getElementById('barBtnPoesia');
|
||
const btnOpenPoesiaHistory = document.getElementById('btnOpenPoesiaHistory');
|
||
const poesiaHistoryModal = document.getElementById('poesiaHistoryModal');
|
||
const btnClosePoesiaHistory = document.getElementById('btnClosePoesiaHistory');
|
||
|
||
const poesiaTemplateSelect = document.getElementById('poesiaTemplateSelect');
|
||
const poesiaTemaInput = document.getElementById('poesiaTemaInput');
|
||
const poesiaTurmaSelect = document.getElementById('poesiaTurmaSelect');
|
||
const poesiaFaixaEtaria = document.getElementById('poesiaFaixaEtaria');
|
||
const poesiaEstruturaSelect = document.getElementById('poesiaEstruturaSelect');
|
||
const poesiaEstiloSelect = document.getElementById('poesiaEstiloSelect');
|
||
const poesiaVozSelect = document.getElementById('poesiaVozSelect');
|
||
const btnGeneratePoesia = document.getElementById('btnGeneratePoesia');
|
||
const poesiaGenerateLoader = document.getElementById('poesiaGenerateLoader');
|
||
|
||
// Elementos do Painel Direito
|
||
const poesiaBnccBadge = document.getElementById('poesiaBnccBadge');
|
||
const poesiaMetricsBox = document.getElementById('poesiaMetricsBox');
|
||
const poesiaEsquemaBadge = document.getElementById('poesiaEsquemaBadge');
|
||
const poesiaMetricRimaVal = document.getElementById('poesiaMetricRimaVal');
|
||
const poesiaMetricRimaBar = document.getElementById('poesiaMetricRimaBar');
|
||
const poesiaMetricRitmoVal = document.getElementById('poesiaMetricRitmoVal');
|
||
const poesiaMetricRitmoBar = document.getElementById('poesiaMetricRitmoBar');
|
||
const poesiaMetricAliteracaoVal = document.getElementById('poesiaMetricAliteracaoVal');
|
||
const poesiaMetricAliteracaoBar = document.getElementById('poesiaMetricAliteracaoBar');
|
||
const poesiaMetricMusicalidadeVal = document.getElementById('poesiaMetricMusicalidadeVal');
|
||
const poesiaMetricMusicalidadeBar = document.getElementById('poesiaMetricMusicalidadeBar');
|
||
const poesiaMetricaDesc = document.getElementById('poesiaMetricaDesc');
|
||
|
||
const poesiaAudioPlayerBox = document.getElementById('poesiaAudioPlayerBox');
|
||
const poesiaAudioPlayer = document.getElementById('poesiaAudioPlayer');
|
||
const poesiaPlayerTitle = document.getElementById('poesiaPlayerTitle');
|
||
const btnDownloadPoesiaAudio = document.getElementById('btnDownloadPoesiaAudio');
|
||
|
||
const poesiaContentTabs = document.getElementById('poesiaContentTabs');
|
||
const btnTabPoesiaOriginal = document.getElementById('btnTabPoesiaOriginal');
|
||
const btnTabPoesiaFonetica = document.getElementById('btnTabPoesiaFonetica');
|
||
const btnTabPoesiaPedagogica = document.getElementById('btnTabPoesiaPedagogica');
|
||
|
||
const poesiaPlaceholderText = document.getElementById('poesiaPlaceholderText');
|
||
const poesiaActiveBox = document.getElementById('poesiaActiveBox');
|
||
const poesiaActiveTitle = document.getElementById('poesiaActiveTitle');
|
||
const poesiaActiveSubtitle = document.getElementById('poesiaActiveSubtitle');
|
||
const poesiaActiveContent = document.getElementById('poesiaActiveContent');
|
||
const poesiaPedagogicalTipBox = document.getElementById('poesiaPedagogicalTipBox');
|
||
const poesiaPedagogicalTipText = document.getElementById('poesiaPedagogicalTipText');
|
||
|
||
const btnCopyActivePoesia = document.getElementById('btnCopyActivePoesia');
|
||
const btnDownloadActivePoesiaTxt = document.getElementById('btnDownloadActivePoesiaTxt');
|
||
const btnSendPoesiaToMusic = document.getElementById('btnSendPoesiaToMusic');
|
||
const btnExportActivePoesiaPdf = document.getElementById('btnExportActivePoesiaPdf');
|
||
|
||
let selectedPoesiaEstrofes = 3;
|
||
let activePoesiaTab = 'original';
|
||
|
||
// TEMPLATES TEMÁTICOS DO ESTÚDIO DE POESIAS
|
||
const POESIA_TEMPLATES = {
|
||
horta: {
|
||
tema: "Plantar sementinhas na horta da escola, regar a terra com carinho, ver os brotinhos crescerem e colher vegetais coloridos e saudáveis.",
|
||
faixa: "Crianças pequenas (4 anos a 5 anos e 11 meses)",
|
||
estrutura: "Quadras (Rimas A-B-A-B)",
|
||
estilo: "Lúdico-cantado e sonoro",
|
||
voz: "mulher",
|
||
estrofes: 3
|
||
},
|
||
animais: {
|
||
tema: "Os bichinhos da fazenda, o canto do galo pela manhã, a vaquinha mimosa, o cachorrinho amigo e as onomatopeias dos sons dos animais.",
|
||
faixa: "Crianças bem pequenas (1 ano e 7 meses a 3 anos e 11 meses)",
|
||
estrutura: "Quadras (Rimas A-B-A-B)",
|
||
estilo: "Lúdico-cantado e sonoro",
|
||
voz: "mulher",
|
||
estrofes: 3
|
||
},
|
||
familia: {
|
||
tema: "O abraço quentinho da família, o carinho ao chegar em casa, os beijinhos de boa noite e o afeto entre pais, avós e irmãos.",
|
||
faixa: "Crianças bem pequenas (1 ano e 7 meses a 3 anos e 11 meses)",
|
||
estrutura: "Redondilhas (7 sílabas poéticas)",
|
||
estilo: "Sensorial, suave e afetivo",
|
||
voz: "mulher",
|
||
estrofes: 3
|
||
},
|
||
higiene: {
|
||
tema: "Bolhas de sabão lavando as mãozinhas, a escova de dentes dançando na boca e o cheirinho gostoso de banho tomado.",
|
||
faixa: "Crianças pequenas (4 anos a 5 anos e 11 meses)",
|
||
estrutura: "Quadras (Rimas A-B-A-B)",
|
||
estilo: "Lúdico-cantado e sonoro",
|
||
voz: "crianca",
|
||
estrofes: 3
|
||
},
|
||
chuva: {
|
||
tema: "As gotinhas de chuva batendo ploc-ploc no telhado, o cheiro de terra molhada e a dança com guarda-chuva colorido.",
|
||
faixa: "Crianças pequenas (4 anos a 5 anos e 11 meses)",
|
||
estrutura: "Redondilhas (7 sílabas poéticas)",
|
||
estilo: "Sensorial, suave e afetivo",
|
||
voz: "mulher",
|
||
estrofes: 3
|
||
},
|
||
alfabeto: {
|
||
tema: "As letrinhas encantadas brincando no papel, as rimas das vogais e a mágica de escrever o primeiro nome.",
|
||
faixa: "Crianças pequenas (4 anos a 5 anos e 11 meses)",
|
||
estrutura: "Quadras (Rimas A-B-A-B)",
|
||
estilo: "Narrativo-infantil com história",
|
||
voz: "mulher",
|
||
estrofes: 4
|
||
},
|
||
cores: {
|
||
tema: "O azul do céu infinito, o amarelo do sol brilhante, o verde das folhinhas e a aquarela das cores na pintura com os dedinhos.",
|
||
faixa: "Crianças pequenas (4 anos a 5 anos e 11 meses)",
|
||
estrutura: "Haiku Infantil (5-7-5 sílabas)",
|
||
estilo: "Sensorial, suave e afetivo",
|
||
voz: "mulher",
|
||
estrofes: 3
|
||
}
|
||
};
|
||
|
||
// Estado da poesia ativa
|
||
let activePoesiaData = {
|
||
titulo: '',
|
||
poesia: '',
|
||
versaoFonetica: '',
|
||
esquemaRima: '',
|
||
analiseLiteraria: { rima: 80, ritmo: 80, aliteracao: 60, musicalidade: 85, explicacaoMetrica: '' },
|
||
bnccCampos: '',
|
||
dicas: '',
|
||
audioUrl: '',
|
||
tema: '',
|
||
estrutura: '',
|
||
estilo: '',
|
||
faixaEtaria: ''
|
||
};
|
||
|
||
// Botões de contagem de estrofes
|
||
document.querySelectorAll('.btn-poesia-estrofes').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
document.querySelectorAll('.btn-poesia-estrofes').forEach(b => b.classList.remove('active'));
|
||
e.target.classList.add('active');
|
||
selectedPoesiaEstrofes = parseInt(e.target.dataset.estrofes);
|
||
});
|
||
});
|
||
|
||
// Carregar turmas no seletor do estúdio de poesia
|
||
const loadTurmasForPoesia = async () => {
|
||
if (!poesiaTurmaSelect) return;
|
||
try {
|
||
const res = await fetch('/api/turmas');
|
||
if (res.ok) {
|
||
const turmas = await res.json();
|
||
poesiaTurmaSelect.innerHTML = '<option value="">-- Selecionar Turma --</option>';
|
||
turmas.forEach(t => {
|
||
const opt = document.createElement('option');
|
||
opt.value = t.id;
|
||
opt.textContent = `${t.nome} (${t.ano || '2026'})`;
|
||
poesiaTurmaSelect.appendChild(opt);
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.warn('Erro ao carregar turmas para poesia:', err);
|
||
}
|
||
};
|
||
|
||
// Abrir Modal
|
||
if (barBtnPoesia) {
|
||
barBtnPoesia.addEventListener('click', () => {
|
||
poesiaModal.style.display = 'flex';
|
||
loadTurmasForPoesia();
|
||
if (!poesiaTemaInput.value) {
|
||
applyPoesiaTemplate('horta');
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnClosePoesiaModal) {
|
||
btnClosePoesiaModal.addEventListener('click', () => {
|
||
poesiaModal.style.display = 'none';
|
||
if (poesiaAudioPlayer) poesiaAudioPlayer.pause();
|
||
});
|
||
}
|
||
|
||
poesiaModal.addEventListener('click', (e) => {
|
||
if (e.target === poesiaModal) {
|
||
poesiaModal.style.display = 'none';
|
||
if (poesiaAudioPlayer) poesiaAudioPlayer.pause();
|
||
}
|
||
});
|
||
|
||
// Aplicar Template
|
||
const applyPoesiaTemplate = (key) => {
|
||
const tmpl = POESIA_TEMPLATES[key];
|
||
if (!tmpl) return;
|
||
|
||
if (poesiaTemaInput) poesiaTemaInput.value = tmpl.tema;
|
||
if (poesiaFaixaEtaria) poesiaFaixaEtaria.value = tmpl.faixa;
|
||
if (poesiaEstruturaSelect) poesiaEstruturaSelect.value = tmpl.estrutura;
|
||
if (poesiaEstiloSelect) poesiaEstiloSelect.value = tmpl.estilo;
|
||
if (poesiaVozSelect) poesiaVozSelect.value = tmpl.voz;
|
||
|
||
// Atualizar botões de estrofes
|
||
document.querySelectorAll('.btn-poesia-estrofes').forEach(b => {
|
||
const isMatch = parseInt(b.dataset.estrofes) === tmpl.estrofes;
|
||
b.classList.toggle('active', isMatch);
|
||
if (isMatch) selectedPoesiaEstrofes = tmpl.estrofes;
|
||
});
|
||
};
|
||
|
||
if (poesiaTemplateSelect) {
|
||
poesiaTemplateSelect.addEventListener('change', (e) => {
|
||
if (e.target.value) {
|
||
applyPoesiaTemplate(e.target.value);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Alternar Abas no Painel Direito
|
||
const switchPoesiaTab = (tab) => {
|
||
activePoesiaTab = tab;
|
||
[btnTabPoesiaOriginal, btnTabPoesiaFonetica, btnTabPoesiaPedagogica].forEach(b => {
|
||
if (b) {
|
||
b.classList.remove('active');
|
||
b.style.background = 'transparent';
|
||
b.style.color = 'var(--text-secondary)';
|
||
}
|
||
});
|
||
|
||
let currentContent = '';
|
||
let subtitleText = `${activePoesiaData.estrutura || 'Quadras'} • ${activePoesiaData.esquemaRima || 'A-B-A-B'}`;
|
||
|
||
if (tab === 'original') {
|
||
if (btnTabPoesiaOriginal) {
|
||
btnTabPoesiaOriginal.classList.add('active');
|
||
btnTabPoesiaOriginal.style.background = 'rgba(234,179,8,0.15)';
|
||
btnTabPoesiaOriginal.style.color = '#fbbf24';
|
||
}
|
||
currentContent = activePoesiaData.poesia;
|
||
} else if (tab === 'fonetica') {
|
||
if (btnTabPoesiaFonetica) {
|
||
btnTabPoesiaFonetica.classList.add('active');
|
||
btnTabPoesiaFonetica.style.background = 'rgba(234,179,8,0.15)';
|
||
btnTabPoesiaFonetica.style.color = '#fbbf24';
|
||
}
|
||
currentContent = activePoesiaData.versaoFonetica || activePoesiaData.poesia;
|
||
subtitleText = '🗣️ Guia Fonético com Sílabas Tônicas e Pausas Rítmicas';
|
||
} else if (tab === 'pedagogica') {
|
||
if (btnTabPoesiaPedagogica) {
|
||
btnTabPoesiaPedagogica.classList.add('active');
|
||
btnTabPoesiaPedagogica.style.background = 'rgba(234,179,8,0.15)';
|
||
btnTabPoesiaPedagogica.style.color = '#fbbf24';
|
||
}
|
||
currentContent = `📚 Campo de Experiência BNCC:\n${activePoesiaData.bnccCampos}\n\n💡 Vivência em Sala de Aula:\n${activePoesiaData.dicas || 'Recite o poema em roda incentivando as crianças a acompanharem o ritmo com palmas.'}`;
|
||
subtitleText = '💡 Vivência Pedagógica & Alinhamento BNCC';
|
||
}
|
||
|
||
if (poesiaActiveSubtitle) poesiaActiveSubtitle.textContent = subtitleText;
|
||
if (poesiaActiveContent) {
|
||
if (tab === 'fonetica') {
|
||
// Converte **palavra** em negrito no HTML
|
||
const formatted = currentContent.replace(/\*\*(.*?)\*\*/g, '<strong style="color: #fbbf24; text-decoration: underline;">$1</strong>');
|
||
poesiaActiveContent.innerHTML = formatted;
|
||
} else {
|
||
poesiaActiveContent.textContent = currentContent;
|
||
}
|
||
}
|
||
};
|
||
|
||
if (btnTabPoesiaOriginal) btnTabPoesiaOriginal.addEventListener('click', () => switchPoesiaTab('original'));
|
||
if (btnTabPoesiaFonetica) btnTabPoesiaFonetica.addEventListener('click', () => switchPoesiaTab('fonetica'));
|
||
if (btnTabPoesiaPedagogica) btnTabPoesiaPedagogica.addEventListener('click', () => switchPoesiaTab('pedagogica'));
|
||
|
||
// Equalizer visualizer quando o áudio recitado toca
|
||
if (poesiaAudioPlayer) {
|
||
poesiaAudioPlayer.addEventListener('play', () => {
|
||
const bars = document.querySelectorAll('#poesiaWaveformVisualizer .wave-bar');
|
||
bars.forEach(b => b.classList.add('wave-active'));
|
||
});
|
||
poesiaAudioPlayer.addEventListener('pause', () => {
|
||
const bars = document.querySelectorAll('#poesiaWaveformVisualizer .wave-bar');
|
||
bars.forEach(b => b.classList.remove('wave-active'));
|
||
});
|
||
}
|
||
|
||
// --- GERAR POESIA (ANÁLISE LITERÁRIA + ÁUDIO RECITADO) ---
|
||
if (btnGeneratePoesia) {
|
||
btnGeneratePoesia.addEventListener('click', async () => {
|
||
const tema = (poesiaTemaInput?.value || '').trim();
|
||
if (!tema) {
|
||
await showCustomAlert('Aviso', 'Por favor, informe a ideia ou tema da poesia.');
|
||
return;
|
||
}
|
||
|
||
btnGeneratePoesia.disabled = true;
|
||
if (poesiaGenerateLoader) poesiaGenerateLoader.style.display = 'flex';
|
||
|
||
try {
|
||
const res = await fetch('/api/poesias/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
tema,
|
||
faixaEtaria: poesiaFaixaEtaria?.value,
|
||
estrofes: selectedPoesiaEstrofes,
|
||
estrutura: poesiaEstruturaSelect?.value || 'Quadras (Rimas A-B-A-B)',
|
||
estilo: poesiaEstiloSelect?.value || 'Lúdico-cantado e sonoro',
|
||
voz: poesiaVozSelect?.value || 'mulher',
|
||
turmaId: poesiaTurmaSelect?.value || null,
|
||
gerarAudio: true
|
||
})
|
||
});
|
||
|
||
const data = await res.json();
|
||
|
||
if (res.ok && data.success) {
|
||
activePoesiaData = {
|
||
titulo: data.titulo || `Poema: ${tema}`,
|
||
poesia: data.poesia,
|
||
versaoFonetica: data.versaoFonetica,
|
||
esquemaRima: data.esquemaRima || 'A-B-A-B',
|
||
analiseLiteraria: data.analiseLiteraria || { rima: 85, ritmo: 80, aliteracao: 65, musicalidade: 90 },
|
||
bnccCampos: data.bnccCampos || 'EI02EF01 / EI03EF02 • Escuta, Fala, Pensamento e Imaginação',
|
||
dicas: data.dicasPedagogicas || '',
|
||
audioUrl: data.audioUrl,
|
||
tema: tema,
|
||
estrutura: data.estrutura || poesiaEstruturaSelect?.value,
|
||
estilo: data.estilo || poesiaEstiloSelect?.value,
|
||
faixaEtaria: data.faixaEtaria || poesiaFaixaEtaria?.value
|
||
};
|
||
|
||
// Atualizar Interface
|
||
if (poesiaPlaceholderText) poesiaPlaceholderText.style.display = 'none';
|
||
if (poesiaMetricsBox) poesiaMetricsBox.style.display = 'flex';
|
||
if (poesiaContentTabs) poesiaContentTabs.style.display = 'flex';
|
||
if (poesiaActiveBox) poesiaActiveBox.style.display = 'flex';
|
||
|
||
if (poesiaActiveTitle) poesiaActiveTitle.textContent = activePoesiaData.titulo;
|
||
if (poesiaBnccBadge) {
|
||
poesiaBnccBadge.textContent = `BNCC: ${activePoesiaData.bnccCampos.split('•')[0].trim()}`;
|
||
poesiaBnccBadge.style.display = 'inline-block';
|
||
}
|
||
if (poesiaEsquemaBadge) {
|
||
poesiaEsquemaBadge.textContent = `Esquema: ${activePoesiaData.esquemaRima}`;
|
||
}
|
||
|
||
// Atualizar Barras de Métricas Literárias
|
||
const metrics = activePoesiaData.analiseLiteraria;
|
||
if (poesiaMetricRimaVal) poesiaMetricRimaVal.textContent = `${metrics.rima || 85}%`;
|
||
if (poesiaMetricRimaBar) poesiaMetricRimaBar.style.width = `${metrics.rima || 85}%`;
|
||
|
||
if (poesiaMetricRitmoVal) poesiaMetricRitmoVal.textContent = `${metrics.ritmo || 80}%`;
|
||
if (poesiaMetricRitmoBar) poesiaMetricRitmoBar.style.width = `${metrics.ritmo || 80}%`;
|
||
|
||
if (poesiaMetricAliteracaoVal) poesiaMetricAliteracaoVal.textContent = `${metrics.aliteracao || 65}%`;
|
||
if (poesiaMetricAliteracaoBar) poesiaMetricAliteracaoBar.style.width = `${metrics.aliteracao || 65}%`;
|
||
|
||
if (poesiaMetricMusicalidadeVal) poesiaMetricMusicalidadeVal.textContent = `${metrics.musicalidade || 90}%`;
|
||
if (poesiaMetricMusicalidadeBar) poesiaMetricMusicalidadeBar.style.width = `${metrics.musicalidade || 90}%`;
|
||
|
||
if (poesiaMetricaDesc && metrics.explicacaoMetrica) {
|
||
poesiaMetricaDesc.textContent = metrics.explicacaoMetrica;
|
||
}
|
||
|
||
if (poesiaPedagogicalTipText && activePoesiaData.dicas) {
|
||
poesiaPedagogicalTipText.textContent = activePoesiaData.dicas;
|
||
poesiaPedagogicalTipBox.style.display = 'block';
|
||
}
|
||
|
||
// Áudio Recitado
|
||
if (data.audioUrl) {
|
||
poesiaAudioPlayer.src = data.audioUrl;
|
||
if (poesiaPlayerTitle) poesiaPlayerTitle.textContent = activePoesiaData.titulo;
|
||
if (btnDownloadPoesiaAudio) btnDownloadPoesiaAudio.href = data.audioUrl;
|
||
if (poesiaAudioPlayerBox) poesiaAudioPlayerBox.style.display = 'flex';
|
||
} else {
|
||
if (poesiaAudioPlayerBox) poesiaAudioPlayerBox.style.display = 'none';
|
||
}
|
||
|
||
switchPoesiaTab('original');
|
||
showToast('Poesia pedagógica e métricas geradas com sucesso!', 'success');
|
||
|
||
} else {
|
||
await showCustomAlert('Erro', data.error || 'Erro ao gerar poesia.');
|
||
}
|
||
|
||
} catch (err) {
|
||
console.error('Erro ao compor poesia:', err);
|
||
await showCustomAlert('Erro', 'Não foi possível conectar ao servidor: ' + err.message);
|
||
} finally {
|
||
btnGeneratePoesia.disabled = false;
|
||
if (poesiaGenerateLoader) poesiaGenerateLoader.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Copiar Poema Ativo
|
||
if (btnCopyActivePoesia) {
|
||
btnCopyActivePoesia.addEventListener('click', () => {
|
||
const text = activePoesiaData.poesia || '';
|
||
if (text) {
|
||
navigator.clipboard.writeText(`${activePoesiaData.titulo}\n\n${text}`);
|
||
showToast('Poema copiado para a área de transferência!', 'success');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Baixar Poema TXT
|
||
if (btnDownloadActivePoesiaTxt) {
|
||
btnDownloadActivePoesiaTxt.addEventListener('click', () => {
|
||
const text = activePoesiaData.poesia || '';
|
||
if (!text) return;
|
||
const content = `POESIA PEDAGÓGICA - PEDAGOGIA\n========================================\nTítulo: ${activePoesiaData.titulo}\nFaixa Etária: ${activePoesiaData.faixaEtaria}\nEstrutura: ${activePoesiaData.estrutura}\nEsquema de Rima: ${activePoesiaData.esquemaRima}\nBNCC: ${activePoesiaData.bnccCampos}\n========================================\n\n${text}\n\n----------------------------------------\nGuia de Leitura / Fonética:\n${activePoesiaData.versaoFonetica}\n\n----------------------------------------\nVivência Pedagógica:\n${activePoesiaData.dicas}\n`;
|
||
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||
const link = document.createElement('a');
|
||
link.href = URL.createObjectURL(blob);
|
||
link.download = `poema_${(activePoesiaData.titulo || 'poesia').replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase()}.txt`;
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
link.remove();
|
||
});
|
||
}
|
||
|
||
// Enviar Poema ao Musicando Ideias
|
||
if (btnSendPoesiaToMusic) {
|
||
btnSendPoesiaToMusic.addEventListener('click', () => {
|
||
poesiaModal.style.display = 'none';
|
||
const barBtnMusica = document.getElementById('barBtnMusica');
|
||
const musicaModal = document.getElementById('musicaModal');
|
||
const musicaTemaInput = document.getElementById('musicaTemaInput');
|
||
if (musicaModal && musicaTemaInput) {
|
||
musicaModal.style.display = 'flex';
|
||
musicaTemaInput.value = `Musicar a poesia "${activePoesiaData.titulo}": ${activePoesiaData.poesia.substring(0, 200)}...`;
|
||
showToast('Poema transferido para o Estúdio de Música!', 'success');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Exportar PDF Ilustrado Formatado
|
||
if (btnExportActivePoesiaPdf) {
|
||
btnExportActivePoesiaPdf.addEventListener('click', () => {
|
||
const text = activePoesiaData.poesia || '';
|
||
if (!text) return;
|
||
|
||
const printWindow = window.open('', '_blank');
|
||
printWindow.document.write(`
|
||
<html>
|
||
<head>
|
||
<title>${activePoesiaData.titulo} - PedaGog</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&family=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
body { font-family: 'Fredoka', 'Outfit', sans-serif; padding: 40px; color: #1e293b; background: #fffdf8; line-height: 1.8; }
|
||
.header-box { border-bottom: 3px dashed #eab308; padding-bottom: 16px; margin-bottom: 24px; }
|
||
h1 { color: #ca8a04; margin: 0 0 8px 0; font-size: 2rem; }
|
||
.meta-tags { display: flex; gap: 12px; font-size: 0.9rem; color: #64748b; font-weight: 600; margin-bottom: 8px; flex-wrap: wrap; }
|
||
.meta-tag { background: #fef9c3; color: #a16207; padding: 4px 10px; border-radius: 6px; }
|
||
.poetry-box { font-size: 1.25rem; background: #ffffff; padding: 28px; border-radius: 12px; border: 1px solid #fef08a; white-space: pre-wrap; margin-bottom: 24px; box-shadow: 0 4px 6px rgba(0,0,0,0.02); }
|
||
.metrics-box { background: #f8fafc; border: 1px solid #e2e8f0; padding: 14px 18px; border-radius: 8px; font-size: 0.85rem; color: #475569; margin-bottom: 20px; }
|
||
.tip-box { background: #fefce8; border: 1px solid #fde047; color: #854d0e; padding: 14px 18px; border-radius: 8px; font-size: 0.92rem; }
|
||
@media print { body { padding: 20px; } button { display: none; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div style="max-width: 750px; margin: 0 auto;">
|
||
<div class="header-box">
|
||
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
|
||
<div>
|
||
<h1>📜 ${activePoesiaData.titulo}</h1>
|
||
<div class="meta-tags">
|
||
<span class="meta-tag">👶 ${activePoesiaData.faixaEtaria}</span>
|
||
<span class="meta-tag">📐 ${activePoesiaData.estrutura}</span>
|
||
<span class="meta-tag">📚 ${activePoesiaData.bnccCampos.split('•')[0]}</span>
|
||
</div>
|
||
</div>
|
||
<button onclick="window.print()" style="background: #eab308; color: #1e293b; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: bold; font-family: sans-serif;">🖨️ Imprimir / Salvar PDF</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="poetry-box">${text}</div>
|
||
|
||
<div class="metrics-box">
|
||
<strong>📊 Análise Métrica:</strong> Rima: ${activePoesiaData.analiseLiteraria.rima}% | Ritmo: ${activePoesiaData.analiseLiteraria.ritmo}% | Musicalidade: ${activePoesiaData.analiseLiteraria.musicalidade}% (Esquema: ${activePoesiaData.esquemaRima})
|
||
</div>
|
||
|
||
${activePoesiaData.dicas ? `
|
||
<div class="tip-box">
|
||
💡 <strong>Vivência Pedagógica / Roda de Leitura:</strong><br>
|
||
${activePoesiaData.dicas}
|
||
</div>` : ''}
|
||
|
||
<div style="margin-top: 30px; text-align: center; font-size: 0.8rem; color: #94a3b8; border-top: 1px solid #e2e8f0; padding-top: 10px;">
|
||
Estúdio de Poesias • PedagogIA • Assistente Pedagógica da Professora Camila
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`);
|
||
printWindow.document.close();
|
||
});
|
||
}
|
||
|
||
// --- HISTÓRICO DE POESIAS ---
|
||
if (btnOpenPoesiaHistory) {
|
||
btnOpenPoesiaHistory.addEventListener('click', () => {
|
||
poesiaHistoryModal.style.display = 'flex';
|
||
loadPoesiaHistory();
|
||
});
|
||
}
|
||
|
||
if (btnClosePoesiaHistory) {
|
||
btnClosePoesiaHistory.addEventListener('click', () => {
|
||
poesiaHistoryModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
poesiaHistoryModal.addEventListener('click', (e) => {
|
||
if (e.target === poesiaHistoryModal) {
|
||
poesiaHistoryModal.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
async function loadPoesiaHistory() {
|
||
const listContainer = document.getElementById('poesiaHistoryList');
|
||
if (!listContainer) return;
|
||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando histórico...</p>';
|
||
|
||
try {
|
||
const response = await fetch('/api/poesias/list');
|
||
const data = await response.json();
|
||
if (data && data.length > 0) {
|
||
listContainer.innerHTML = '';
|
||
data.forEach(item => {
|
||
const date = new Date(item.created_at).toLocaleString('pt-BR');
|
||
const div = document.createElement('div');
|
||
div.className = 'history-item-card';
|
||
div.style.cssText = 'background: rgba(255,255,255,0.03); padding: 14px; border-radius: 10px; border: 1px solid var(--border-light); display: flex; flex-direction: column; gap: 8px; cursor: pointer; position: relative; transition: all 0.2s;';
|
||
div.innerHTML = `
|
||
<div style="display: flex; justify-content: space-between; align-items: flex-start; padding-right: 30px;">
|
||
<strong style="color: #fbbf24; font-size: 0.95rem;">${item.faixa_etaria} (${item.estrofes} Estrofes)</strong>
|
||
<span style="font-size: 0.72rem; color: var(--text-secondary);">${date}</span>
|
||
</div>
|
||
<p style="margin: 0; font-size: 0.85rem; color: var(--text-primary); opacity: 0.9;"><strong>Tema:</strong> ${item.tema}</p>
|
||
${item.audio_url ? `
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
|
||
<span style="font-size: 0.75rem; color: #10b981; font-weight: 600;">🎙️ Áudio Recitado Disponível</span>
|
||
</div>
|
||
` : ''}
|
||
<button class="btn-delete-poesia" data-id="${item.id}" style="position: absolute; top: 12px; right: 12px; background: transparent; border: none; color: #ef4444; cursor: pointer; font-size: 1.1rem; padding: 4px; border-radius: 4px;" title="Excluir Poesia">🗑️</button>
|
||
`;
|
||
|
||
div.addEventListener('click', async (e) => {
|
||
if (e.target.closest('.btn-delete-poesia')) {
|
||
e.stopPropagation();
|
||
if (confirm('Tem certeza que deseja excluir esta poesia do histórico?')) {
|
||
try {
|
||
const delRes = await fetch(`/api/poesias/${item.id}`, { method: 'DELETE' });
|
||
if (delRes.ok) {
|
||
showToast('Poesia excluída do histórico.', 'success');
|
||
loadPoesiaHistory();
|
||
}
|
||
} catch (err) {
|
||
showToast('Erro ao excluir poesia.', 'error');
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Carregar poesia clicada
|
||
try {
|
||
const res = await fetch(`/api/poesias/${item.id}`);
|
||
const detail = await res.json();
|
||
if (res.ok) {
|
||
activePoesiaData = {
|
||
titulo: `Poema: ${detail.tema}`,
|
||
poesia: detail.poesia,
|
||
versaoFonetica: detail.versao_fonetica || detail.poesia,
|
||
esquemaRima: 'A-B-A-B',
|
||
analiseLiteraria: typeof detail.analise_metrica === 'string' ? JSON.parse(detail.analise_metrica) : (detail.analise_metrica || { rima: 85, ritmo: 80, aliteracao: 65, musicalidade: 90 }),
|
||
bnccCampos: detail.bncc || 'EI02EF01 / EI03EF02 • Escuta, Fala, Pensamento e Imaginação',
|
||
dicas: detail.dicas || '',
|
||
audioUrl: detail.audio_url,
|
||
tema: detail.tema,
|
||
estrutura: detail.estrutura || 'Quadras',
|
||
estilo: detail.estilo || 'Lúdico-cantado',
|
||
faixaEtaria: detail.faixa_etaria
|
||
};
|
||
|
||
if (poesiaPlaceholderText) poesiaPlaceholderText.style.display = 'none';
|
||
if (poesiaMetricsBox) poesiaMetricsBox.style.display = 'flex';
|
||
if (poesiaContentTabs) poesiaContentTabs.style.display = 'flex';
|
||
if (poesiaActiveBox) poesiaActiveBox.style.display = 'flex';
|
||
|
||
if (poesiaActiveTitle) poesiaActiveTitle.textContent = activePoesiaData.titulo;
|
||
if (detail.audio_url) {
|
||
poesiaAudioPlayer.src = detail.audio_url;
|
||
if (btnDownloadPoesiaAudio) btnDownloadPoesiaAudio.href = detail.audio_url;
|
||
if (poesiaAudioPlayerBox) poesiaAudioPlayerBox.style.display = 'flex';
|
||
} else {
|
||
if (poesiaAudioPlayerBox) poesiaAudioPlayerBox.style.display = 'none';
|
||
}
|
||
|
||
switchPoesiaTab('original');
|
||
poesiaHistoryModal.style.display = 'none';
|
||
}
|
||
} catch (err) {
|
||
showToast('Erro ao carregar detalhes da poesia.', 'error');
|
||
}
|
||
});
|
||
|
||
listContainer.appendChild(div);
|
||
});
|
||
} else {
|
||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary); padding: 20px;">Nenhuma poesia gerada até o momento.</p>';
|
||
}
|
||
} catch (err) {
|
||
listContainer.innerHTML = '<p style="text-align:center; color:#ef4444;">Erro ao carregar histórico de poesias.</p>';
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// BRINCARATIVO (CIRCUITOS PSICOMOTORES E BRINCADEIRAS)
|
||
// ============================================================
|
||
const brincarAtivoModal = document.getElementById('brincarAtivoModal');
|
||
const btnCloseBrincarAtivoModal = document.getElementById('btnCloseBrincarAtivoModal');
|
||
const barBtnBrincarAtivo = document.getElementById('barBtnBrincarAtivo');
|
||
|
||
const tabBrincarAtivoNova = document.getElementById('tabBrincarAtivoNova');
|
||
const tabBrincarAtivoHistorico = document.getElementById('tabBrincarAtivoHistorico');
|
||
const contentBrincarAtivoNova = document.getElementById('contentBrincarAtivoNova');
|
||
const contentBrincarAtivoHistorico = document.getElementById('contentBrincarAtivoHistorico');
|
||
|
||
const brincarAtivoFaixaSelect = document.getElementById('brincarAtivoFaixaSelect');
|
||
const brincarAtivoRecursosInput = document.getElementById('brincarAtivoRecursosInput');
|
||
const btnGenerateBrincarAtivo = document.getElementById('btnGenerateBrincarAtivo');
|
||
const brincarAtivoLoader = document.getElementById('brincarAtivoLoader');
|
||
const brincarAtivoLoaderText = document.getElementById('brincarAtivoLoaderText');
|
||
const brincarAtivoResultArea = document.getElementById('brincarAtivoResultArea');
|
||
const brincarAtivoReportArea = document.getElementById('brincarAtivoReportArea');
|
||
|
||
const btnCopyBrincarAtivoReport = document.getElementById('btnCopyBrincarAtivoReport');
|
||
const btnPdfBrincarAtivoReport = document.getElementById('btnPdfBrincarAtivoReport');
|
||
const brincarAtivoListContainer = document.getElementById('brincarAtivoListContainer');
|
||
const brincarAtivoNoData = document.getElementById('brincarAtivoNoData');
|
||
|
||
let activeBrincarAtivoReport = null;
|
||
|
||
// Abas do BrincarAtivo
|
||
if (tabBrincarAtivoNova) {
|
||
tabBrincarAtivoNova.addEventListener('click', () => {
|
||
tabBrincarAtivoNova.classList.add('active');
|
||
tabBrincarAtivoHistorico.classList.remove('active');
|
||
contentBrincarAtivoNova.style.display = 'block';
|
||
contentBrincarAtivoHistorico.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
if (tabBrincarAtivoHistorico) {
|
||
tabBrincarAtivoHistorico.addEventListener('click', () => {
|
||
tabBrincarAtivoHistorico.classList.add('active');
|
||
tabBrincarAtivoNova.classList.remove('active');
|
||
contentBrincarAtivoNova.style.display = 'none';
|
||
contentBrincarAtivoHistorico.style.display = 'block';
|
||
loadBrincarAtivoHistory();
|
||
});
|
||
}
|
||
|
||
// Abrir Modal
|
||
if (barBtnBrincarAtivo) {
|
||
barBtnBrincarAtivo.addEventListener('click', () => {
|
||
brincarAtivoModal.style.display = 'flex';
|
||
brincarAtivoRecursosInput.value = '';
|
||
brincarAtivoResultArea.style.display = 'none';
|
||
brincarAtivoLoader.style.display = 'none';
|
||
btnGenerateBrincarAtivo.disabled = false;
|
||
activeBrincarAtivoReport = null;
|
||
if (tabBrincarAtivoNova) tabBrincarAtivoNova.click();
|
||
});
|
||
}
|
||
|
||
// Fechar Modal
|
||
if (btnCloseBrincarAtivoModal) {
|
||
btnCloseBrincarAtivoModal.addEventListener('click', () => {
|
||
brincarAtivoModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
window.addEventListener('click', (e) => {
|
||
if (e.target === brincarAtivoModal) {
|
||
brincarAtivoModal.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// Gerar Atividade
|
||
if (btnGenerateBrincarAtivo) {
|
||
btnGenerateBrincarAtivo.addEventListener('click', async () => {
|
||
const faixaEtaria = brincarAtivoFaixaSelect.value;
|
||
const recursos = brincarAtivoRecursosInput.value.trim();
|
||
|
||
if (!recursos) {
|
||
alert('Por favor, informe os materiais ou recursos disponíveis.');
|
||
return;
|
||
}
|
||
|
||
btnGenerateBrincarAtivo.disabled = true;
|
||
brincarAtivoLoader.style.display = 'flex';
|
||
brincarAtivoResultArea.style.display = 'none';
|
||
brincarAtivoLoaderText.textContent = 'Acessando referências BNCC...';
|
||
|
||
let statusMsgIdx = 0;
|
||
const statusMessages = [
|
||
'Analisando faixa etária psicomotora...',
|
||
'Estruturando proposta lúdica de circuito...',
|
||
'Conectando objetivos específicos da BNCC...',
|
||
'Gerando instruções passo a passo para o professor...',
|
||
'Finalizando plano de circuito ativo...'
|
||
];
|
||
|
||
const statusInterval = setInterval(() => {
|
||
if (statusMsgIdx < statusMessages.length) {
|
||
brincarAtivoLoaderText.textContent = statusMessages[statusMsgIdx];
|
||
statusMsgIdx++;
|
||
} else {
|
||
brincarAtivoLoaderText.textContent = 'Formatando atividade...';
|
||
}
|
||
}, 4500);
|
||
|
||
try {
|
||
const response = await fetch('/api/brincar-ativo/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ faixaEtaria, recursos })
|
||
});
|
||
|
||
clearInterval(statusInterval);
|
||
|
||
if (!response.ok) {
|
||
const errData = await response.json();
|
||
throw new Error(errData.error || 'Erro desconhecido');
|
||
}
|
||
|
||
const data = await response.json();
|
||
activeBrincarAtivoReport = data;
|
||
|
||
if (window.marked) {
|
||
brincarAtivoReportArea.innerHTML = marked.parse(data.atividade);
|
||
} else {
|
||
brincarAtivoReportArea.textContent = data.atividade;
|
||
}
|
||
|
||
brincarAtivoLoader.style.display = 'none';
|
||
brincarAtivoResultArea.style.display = 'flex';
|
||
btnGenerateBrincarAtivo.disabled = false;
|
||
|
||
} catch (err) {
|
||
clearInterval(statusInterval);
|
||
console.error(err);
|
||
alert('Erro ao gerar atividade: ' + err.message);
|
||
brincarAtivoLoader.style.display = 'none';
|
||
btnGenerateBrincarAtivo.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Copiar Parecer BrincarAtivo
|
||
if (btnCopyBrincarAtivoReport) {
|
||
btnCopyBrincarAtivoReport.addEventListener('click', () => {
|
||
if (activeBrincarAtivoReport && activeBrincarAtivoReport.atividade) {
|
||
navigator.clipboard.writeText(activeBrincarAtivoReport.atividade)
|
||
.then(() => {
|
||
btnCopyBrincarAtivoReport.textContent = '✅ Copiado!';
|
||
setTimeout(() => { btnCopyBrincarAtivoReport.textContent = '📋 Copiar'; }, 2000);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// Baixar PDF BrincarAtivo
|
||
if (btnPdfBrincarAtivoReport) {
|
||
btnPdfBrincarAtivoReport.addEventListener('click', () => {
|
||
if (!activeBrincarAtivoReport) return;
|
||
const printWindow = window.open('', '_blank');
|
||
printWindow.document.write(`
|
||
<html>
|
||
<head>
|
||
<title>BrincarAtivo - Roteiro de Circuito</title>
|
||
<style>
|
||
body { font-family: 'Outfit', 'Inter', sans-serif; padding: 40px; color: #333; line-height: 1.6; }
|
||
h1, h2, h3 { color: #10a37f; margin-top: 24px; margin-bottom: 12px; }
|
||
h1 { border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; font-size: 22px; }
|
||
.meta-box { background: #f8fafc; padding: 16px; border-radius: 8px; margin-bottom: 24px; border: 1px solid #e2e8f0; }
|
||
.meta-label { font-weight: bold; color: #475569; }
|
||
@media print {
|
||
body { padding: 0; }
|
||
button { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>🏃♂️ BrincarAtivo - Planejamento Psicomotor</h1>
|
||
<div class="meta-box">
|
||
<div><span class="meta-label">Faixa Etária:</span> <span>${activeBrincarAtivoReport.faixaEtaria}</span></div>
|
||
<div><span class="meta-label">Recursos:</span> <span>${activeBrincarAtivoReport.recursos}</span></div>
|
||
<div><span class="meta-label">Data de Criação:</span> <span>${new Date(activeBrincarAtivoReport.createdAt).toLocaleDateString('pt-BR')}</span></div>
|
||
</div>
|
||
<div>${window.marked ? marked.parse(activeBrincarAtivoReport.atividade) : activeBrincarAtivoReport.atividade}</div>
|
||
<script>
|
||
window.onload = function() {
|
||
window.print();
|
||
setTimeout(() => { window.close(); }, 500);
|
||
};
|
||
<\/script>
|
||
</body>
|
||
</html>
|
||
`);
|
||
printWindow.document.close();
|
||
});
|
||
}
|
||
|
||
// Carregar Histórico BrincarAtivo
|
||
async function loadBrincarAtivoHistory() {
|
||
try {
|
||
const response = await fetch('/api/brincar-ativo/list');
|
||
if (!response.ok) throw new Error('Erro ao carregar histórico');
|
||
const list = await response.json();
|
||
|
||
brincarAtivoListContainer.innerHTML = '';
|
||
if (list.length === 0) {
|
||
brincarAtivoNoData.style.display = 'block';
|
||
return;
|
||
}
|
||
brincarAtivoNoData.style.display = 'none';
|
||
|
||
list.forEach(item => {
|
||
const div = document.createElement('div');
|
||
div.className = 'quick-note-card';
|
||
div.style.cursor = 'pointer';
|
||
div.style.display = 'flex';
|
||
div.style.flexDirection = 'column';
|
||
div.style.gap = '6px';
|
||
div.style.padding = '12px';
|
||
|
||
const lines = item.recursos || '';
|
||
const previewText = lines.length > 60 ? lines.substring(0, 60) + '...' : lines;
|
||
|
||
div.innerHTML = `
|
||
<div style="font-weight: 600; color: var(--text-primary); font-size: 0.92rem; display: flex; align-items: center; justify-content: space-between; gap: 6px;">
|
||
<span>🏃♂️ ${escapeHtml(item.faixaEtaria.split(' ')[0])} (${item.faixaEtaria.includes('bem pequenas') ? 'Bem Pequenas' : (item.faixaEtaria.includes('pequenas') ? 'Pequenas' : 'Bebês')})</span>
|
||
<button class="delete-btn" data-id="${item.id}" style="background: transparent; border: none; color: var(--text-muted); cursor: pointer; font-size: 0.95rem; padding: 2px 6px; border-radius: 4px; transition: color 0.2s, background 0.2s; display: flex; align-items: center; justify-content: center; z-index: 10;" title="Deletar atividade" onmouseover="this.style.color='#ff4d4f'; this.style.background='rgba(255, 77, 79, 0.1)'" onmouseout="this.style.color='var(--text-muted)'; this.style.background='transparent'">
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
<div style="font-size: 0.8rem; color: var(--text-secondary);">Recursos: ${escapeHtml(previewText)}</div>
|
||
<div style="font-size: 0.75rem; color: var(--text-muted);">${new Date(item.createdAt).toLocaleDateString('pt-BR')}</div>
|
||
`;
|
||
|
||
const deleteBtn = div.querySelector('.delete-btn');
|
||
deleteBtn.addEventListener('click', async (e) => {
|
||
e.stopPropagation();
|
||
if (confirm(`Deseja realmente excluir esta atividade psicomotora do histórico?`)) {
|
||
try {
|
||
const delResp = await fetch(`/api/brincar-ativo/${item.id}`, { method: 'DELETE' });
|
||
if (!delResp.ok) throw new Error('Falha ao deletar');
|
||
alert('Atividade deletada com sucesso.');
|
||
loadBrincarAtivoHistory();
|
||
if (activeBrincarAtivoReport && activeBrincarAtivoReport.id === item.id) {
|
||
activeBrincarAtivoReport = null;
|
||
brincarAtivoFaixaSelect.value = '';
|
||
brincarAtivoRecursosInput.value = '';
|
||
brincarAtivoReportArea.innerHTML = '';
|
||
brincarAtivoResultArea.style.display = 'none';
|
||
}
|
||
} catch (err) {
|
||
alert('Erro ao excluir atividade: ' + err.message);
|
||
}
|
||
}
|
||
});
|
||
|
||
div.addEventListener('click', async () => {
|
||
try {
|
||
const detailResp = await fetch(`/api/brincar-ativo/${item.id}`);
|
||
if (!detailResp.ok) throw new Error('Erro ao obter detalhes');
|
||
const detail = await detailResp.json();
|
||
|
||
activeBrincarAtivoReport = detail;
|
||
brincarAtivoFaixaSelect.value = detail.faixaEtaria;
|
||
brincarAtivoRecursosInput.value = detail.recursos;
|
||
brincarAtivoReportArea.innerHTML = window.marked ? marked.parse(detail.atividade) : detail.atividade;
|
||
|
||
brincarAtivoResultArea.style.display = 'flex';
|
||
if (tabBrincarAtivoNova) tabBrincarAtivoNova.click();
|
||
} catch (e) {
|
||
alert('Falha ao abrir atividade.');
|
||
}
|
||
});
|
||
|
||
brincarAtivoListContainer.appendChild(div);
|
||
});
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
}
|
||
};
|
||
|
||
// Player de áudio personalizado global para as mídias geradas
|
||
window.toggleCustomAudio = (btn) => {
|
||
const player = btn.closest('.custom-audio-player');
|
||
const audio = player.querySelector('audio');
|
||
const playIcon = btn.querySelector('.play-icon');
|
||
const pauseIcon = btn.querySelector('.pause-icon');
|
||
const progress = player.querySelector('.audio-progress');
|
||
const timeSpan = player.querySelector('.audio-time');
|
||
|
||
if (audio.paused) {
|
||
// Pausar outros players que possam estar ativos
|
||
document.querySelectorAll('.custom-audio-player audio').forEach(a => {
|
||
if (a !== audio && !a.paused) {
|
||
a.pause();
|
||
const otherBtn = a.closest('.custom-audio-player').querySelector('.audio-play-btn');
|
||
otherBtn.querySelector('.play-icon').classList.remove('hidden');
|
||
otherBtn.querySelector('.pause-icon').classList.add('hidden');
|
||
}
|
||
});
|
||
|
||
audio.play();
|
||
playIcon.classList.add('hidden');
|
||
pauseIcon.classList.remove('hidden');
|
||
} else {
|
||
audio.pause();
|
||
playIcon.classList.remove('hidden');
|
||
pauseIcon.classList.add('hidden');
|
||
}
|
||
|
||
// Atualizar progresso em tempo real
|
||
audio.ontimeupdate = () => {
|
||
if (audio.duration) {
|
||
const percent = (audio.currentTime / audio.duration) * 100;
|
||
progress.style.width = percent + '%';
|
||
|
||
const mins = Math.floor(audio.currentTime / 60);
|
||
const secs = Math.floor(audio.currentTime % 60).toString().padStart(2, '0');
|
||
timeSpan.textContent = `${mins}:${secs}`;
|
||
}
|
||
};
|
||
|
||
audio.onended = () => {
|
||
playIcon.classList.remove('hidden');
|
||
pauseIcon.classList.add('hidden');
|
||
progress.style.width = '0%';
|
||
timeSpan.textContent = '0:00';
|
||
};
|
||
};
|
||
// ============================================================
|
||
// MUSICANDO IDEIAS (ESTÚDIO PROFISSIONAL)
|
||
// ============================================================
|
||
const barBtnMusica = document.getElementById('barBtnMusica');
|
||
const musicaModal = document.getElementById('musicaModal');
|
||
const btnCloseMusicaModal = document.getElementById('btnCloseMusicaModal');
|
||
const btnOpenMusicaHistory = document.getElementById('btnOpenMusicaHistory');
|
||
const musicaHistoryModal = document.getElementById('musicaHistoryModal');
|
||
const btnCloseMusicaHistory = document.getElementById('btnCloseMusicaHistory');
|
||
|
||
const musicaTemplateSelect = document.getElementById('musicaTemplateSelect');
|
||
const musicaTemaInput = document.getElementById('musicaTemaInput');
|
||
const musicaTurmaSelect = document.getElementById('musicaTurmaSelect');
|
||
const musicaFaixaEtaria = document.getElementById('musicaFaixaEtaria');
|
||
const musicaRitmo = document.getElementById('musicaRitmo');
|
||
const musicaIntensidade = document.getElementById('musicaIntensidade');
|
||
const musicaVozSelect = document.getElementById('musicaVozSelect');
|
||
const musicaDuracaoSelect = document.getElementById('musicaDuracaoSelect');
|
||
const btnGenerateMusica = document.getElementById('btnGenerateMusica');
|
||
const musicaGenerateLoader = document.getElementById('musicaGenerateLoader');
|
||
|
||
// Elementos do Lado Direito
|
||
const musicaBnccBadge = document.getElementById('musicaBnccBadge');
|
||
const musicaAudioPlayerBox = document.getElementById('musicaAudioPlayerBox');
|
||
const musicaAudioPlayer = document.getElementById('musicaAudioPlayer');
|
||
const musicaPlayerTitle = document.getElementById('musicaPlayerTitle');
|
||
const btnToggleMusicLoop = document.getElementById('btnToggleMusicLoop');
|
||
const btnDownloadMusicaAudio = document.getElementById('btnDownloadMusicaAudio');
|
||
const musicaWaveformVisualizer = document.getElementById('musicaWaveformVisualizer');
|
||
|
||
const musicaVersionsTabs = document.getElementById('musicaVersionsTabs');
|
||
const btnTabVersaoPrincipal = document.getElementById('btnTabVersaoPrincipal');
|
||
const btnTabVersaoCurta = document.getElementById('btnTabVersaoCurta');
|
||
const btnTabVersaoDancante = document.getElementById('btnTabVersaoDancante');
|
||
|
||
const musicaPlaceholderText = document.getElementById('musicaPlaceholderText');
|
||
const musicaActiveContentBox = document.getElementById('musicaActiveContentBox');
|
||
const musicaActiveTitle = document.getElementById('musicaActiveTitle');
|
||
const musicaActiveBadge = document.getElementById('musicaActiveBadge');
|
||
const musicaActiveLyrics = document.getElementById('musicaActiveLyrics');
|
||
const musicaPedagogicalTipBox = document.getElementById('musicaPedagogicalTipBox');
|
||
const musicaPedagogicalTipText = document.getElementById('musicaPedagogicalTipText');
|
||
|
||
const btnCopyActiveLyrics = document.getElementById('btnCopyActiveLyrics');
|
||
const btnDownloadActiveTxt = document.getElementById('btnDownloadActiveTxt');
|
||
const btnExportActivePdf = document.getElementById('btnExportActivePdf');
|
||
|
||
// TEMPLATES PRÉ-CONFIGURADOS DO MUSICANDO IDEIAS
|
||
const MUSICA_TEMPLATES = {
|
||
horta: {
|
||
tema: "Plantar sementinhas na horta da escola, regar a terra com carinho, ver os brotinhos crescerem e colher vegetais coloridos e saudáveis.",
|
||
faixa: "Crianças pequenas (4 anos a 5 anos e 11 meses)",
|
||
ritmo: "Cantiga de Roda",
|
||
intensidade: "moderada",
|
||
voz: "mulher",
|
||
duracao: "1min",
|
||
instrumentos: ["Violão acústico", "Flauta doce", "Pandeiro e percussão", "Xilofone infantil"]
|
||
},
|
||
familia: {
|
||
tema: "O amor e o carinho da família, os abraços acolhedores na chegada e na saída da escola, gratidão e respeito aos pais e avós.",
|
||
faixa: "Crianças bem pequenas (1 ano e 7 meses a 3 anos e 11 meses)",
|
||
ritmo: "Canção de Ninar",
|
||
intensidade: "calma",
|
||
voz: "mulher",
|
||
duracao: "1min",
|
||
instrumentos: ["Violão acústico", "Metalofone e sinos", "Flauta doce"]
|
||
},
|
||
higiene: {
|
||
tema: "Lavar as mãozinhas com água e sabão antes do lanche, escovar os dentinhos após comer e cuidar do corpinho limpinho e cheiroso.",
|
||
faixa: "Crianças pequenas (4 anos a 5 anos e 11 meses)",
|
||
ritmo: "Pop Infantil Alegre",
|
||
intensidade: "dancante",
|
||
voz: "crianca",
|
||
duracao: "1min",
|
||
instrumentos: ["Teclado e sintetizador", "Pandeiro e percussão", "Palmas e coro de crianças"]
|
||
},
|
||
mindlab: {
|
||
tema: "Pensar antes de jogar, esperar a sua vez com calma, cooperar com os colegas de equipe e comemorar as jogadas inteligentes.",
|
||
faixa: "Crianças pequenas (4 anos a 5 anos e 11 meses)",
|
||
ritmo: "Cantiga de Roda",
|
||
intensidade: "moderada",
|
||
voz: "homem",
|
||
duracao: "1min",
|
||
instrumentos: ["Violão acústico", "Pandeiro e percussão", "Xilofone infantil"]
|
||
},
|
||
verao: {
|
||
tema: "O sol brilhando no céu, brincadeiras com água e areia no parque, beber água fresca e sentir a brisa da natureza.",
|
||
faixa: "Crianças pequenas (4 anos a 5 anos e 11 meses)",
|
||
ritmo: "Forrózinho Lúdico",
|
||
intensidade: "dancante",
|
||
voz: "mulher",
|
||
duracao: "1min",
|
||
instrumentos: ["Acordeon", "Pandeiro e percussão", "Violão acústico", "Palmas e coro de crianças"]
|
||
},
|
||
alfabeto: {
|
||
tema: "As letrinhas do alfabeto dançando, descobrindo os sons das vogais A E I O U e escrevendo o próprio nome com alegria.",
|
||
faixa: "Crianças pequenas (4 anos a 5 anos e 11 meses)",
|
||
ritmo: "Marchinha Escolar",
|
||
intensidade: "dancante",
|
||
voz: "mulher",
|
||
duracao: "1min",
|
||
instrumentos: ["Pandeiro e percussão", "Metalofone e sinos", "Palmas e coro de crianças"]
|
||
},
|
||
emocoes: {
|
||
tema: "Reconhecer quando estamos felizes, com medo, bravos ou tranquilos, respirar fundo e receber um abraço amigo na roda.",
|
||
faixa: "Crianças bem pequenas (1 ano e 7 meses a 3 anos e 11 meses)",
|
||
ritmo: "Canção de Ninar",
|
||
intensidade: "calma",
|
||
voz: "mulher",
|
||
duracao: "1min",
|
||
instrumentos: ["Violão acústico", "Flauta doce", "Metalofone e sinos"]
|
||
}
|
||
};
|
||
|
||
// Estado atual da música gerada
|
||
let activeMusicaData = {
|
||
titulo: '',
|
||
versaoPrincipal: '',
|
||
versaoCurta: '',
|
||
versaoDancante: '',
|
||
bnccCampos: '',
|
||
instrumentos: '',
|
||
dicas: '',
|
||
audioUrl: '',
|
||
tema: '',
|
||
ritmo: '',
|
||
faixaEtaria: ''
|
||
};
|
||
let activeMusicaTab = 'principal';
|
||
let isLoopActive = false;
|
||
|
||
// Carregar turmas no seletor
|
||
const loadTurmasForMusica = async () => {
|
||
if (!musicaTurmaSelect) return;
|
||
try {
|
||
const res = await fetch('/api/turmas');
|
||
if (res.ok) {
|
||
const turmas = await res.json();
|
||
musicaTurmaSelect.innerHTML = '<option value="">-- Selecionar Turma --</option>';
|
||
turmas.forEach(t => {
|
||
const opt = document.createElement('option');
|
||
opt.value = t.id;
|
||
opt.textContent = `${t.nome} (${t.ano || '2026'})`;
|
||
opt.dataset.nome = t.nome;
|
||
musicaTurmaSelect.appendChild(opt);
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.warn('Erro ao carregar turmas para música:', err);
|
||
}
|
||
};
|
||
|
||
// Abrir Modal
|
||
if (barBtnMusica) {
|
||
barBtnMusica.addEventListener('click', () => {
|
||
musicaModal.style.display = 'flex';
|
||
loadTurmasForMusica();
|
||
if (!musicaTemaInput.value) {
|
||
applyMusicaTemplate('horta');
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnCloseMusicaModal) {
|
||
btnCloseMusicaModal.addEventListener('click', () => {
|
||
musicaModal.style.display = 'none';
|
||
if (musicaAudioPlayer) musicaAudioPlayer.pause();
|
||
});
|
||
}
|
||
|
||
musicaModal.addEventListener('click', (e) => {
|
||
if (e.target === musicaModal) {
|
||
musicaModal.style.display = 'none';
|
||
if (musicaAudioPlayer) musicaAudioPlayer.pause();
|
||
}
|
||
});
|
||
|
||
// Aplicar Template
|
||
const applyMusicaTemplate = (key) => {
|
||
const tmpl = MUSICA_TEMPLATES[key];
|
||
if (!tmpl) return;
|
||
|
||
if (musicaTemaInput) musicaTemaInput.value = tmpl.tema;
|
||
if (musicaFaixaEtaria) musicaFaixaEtaria.value = tmpl.faixa;
|
||
if (musicaRitmo) musicaRitmo.value = tmpl.ritmo;
|
||
if (musicaIntensidade) musicaIntensidade.value = tmpl.intensidade;
|
||
if (musicaVozSelect) musicaVozSelect.value = tmpl.voz;
|
||
if (musicaDuracaoSelect) musicaDuracaoSelect.value = tmpl.duracao;
|
||
|
||
// Ajustar checkboxes de instrumentos
|
||
const checkboxes = document.querySelectorAll('#musicaInstrumentsContainer input[type="checkbox"]');
|
||
checkboxes.forEach(cb => {
|
||
cb.checked = tmpl.instrumentos.includes(cb.value);
|
||
});
|
||
};
|
||
|
||
if (musicaTemplateSelect) {
|
||
musicaTemplateSelect.addEventListener('change', (e) => {
|
||
if (e.target.value) {
|
||
applyMusicaTemplate(e.target.value);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Alternar Versões no Painel Direito
|
||
const switchMusicaVersionTab = (tab) => {
|
||
activeMusicaTab = tab;
|
||
[btnTabVersaoPrincipal, btnTabVersaoCurta, btnTabVersaoDancante].forEach(b => {
|
||
if (b) {
|
||
b.classList.remove('active');
|
||
b.style.background = 'transparent';
|
||
b.style.color = 'var(--text-secondary)';
|
||
}
|
||
});
|
||
|
||
let currentLyrics = '';
|
||
let badgeText = '';
|
||
|
||
if (tab === 'principal') {
|
||
if (btnTabVersaoPrincipal) {
|
||
btnTabVersaoPrincipal.classList.add('active');
|
||
btnTabVersaoPrincipal.style.background = 'rgba(168,85,247,0.15)';
|
||
btnTabVersaoPrincipal.style.color = '#c084fc';
|
||
}
|
||
currentLyrics = activeMusicaData.versaoPrincipal;
|
||
badgeText = '🌟 Versão Completa (com gestos)';
|
||
} else if (tab === 'curta') {
|
||
if (btnTabVersaoCurta) {
|
||
btnTabVersaoCurta.classList.add('active');
|
||
btnTabVersaoCurta.style.background = 'rgba(168,85,247,0.15)';
|
||
btnTabVersaoCurta.style.color = '#c084fc';
|
||
}
|
||
currentLyrics = activeMusicaData.versaoCurta;
|
||
badgeText = '⚡ Rascunho / Memorização Rápida';
|
||
} else if (tab === 'dancante') {
|
||
if (btnTabVersaoDancante) {
|
||
btnTabVersaoDancante.classList.add('active');
|
||
btnTabVersaoDancante.style.background = 'rgba(168,85,247,0.15)';
|
||
btnTabVersaoDancante.style.color = '#c084fc';
|
||
}
|
||
currentLyrics = activeMusicaData.versaoDancante;
|
||
badgeText = '🎉 Versão Dançante / Roda';
|
||
}
|
||
|
||
if (musicaActiveBadge) musicaActiveBadge.textContent = badgeText;
|
||
if (musicaActiveLyrics) musicaActiveLyrics.textContent = currentLyrics || 'Nenhuma letra para esta versão.';
|
||
};
|
||
|
||
if (btnTabVersaoPrincipal) btnTabVersaoPrincipal.addEventListener('click', () => switchMusicaVersionTab('principal'));
|
||
if (btnTabVersaoCurta) btnTabVersaoCurta.addEventListener('click', () => switchMusicaVersionTab('curta'));
|
||
if (btnTabVersaoDancante) btnTabVersaoDancante.addEventListener('click', () => switchMusicaVersionTab('dancante'));
|
||
|
||
// Controle de Loop de Áudio
|
||
if (btnToggleMusicLoop) {
|
||
btnToggleMusicLoop.addEventListener('click', () => {
|
||
isLoopActive = !isLoopActive;
|
||
if (musicaAudioPlayer) musicaAudioPlayer.loop = isLoopActive;
|
||
btnToggleMusicLoop.textContent = isLoopActive ? '🔁 Loop: LIGADO' : '🔁 Loop: Desligado';
|
||
btnToggleMusicLoop.style.background = isLoopActive ? 'rgba(16, 185, 129, 0.2)' : 'rgba(255,255,255,0.05)';
|
||
btnToggleMusicLoop.style.color = isLoopActive ? '#34d399' : 'var(--text-secondary)';
|
||
});
|
||
}
|
||
|
||
// Equalizer / Waveform Animation quando toca
|
||
if (musicaAudioPlayer) {
|
||
musicaAudioPlayer.addEventListener('play', () => {
|
||
const bars = document.querySelectorAll('#musicaWaveformVisualizer .wave-bar');
|
||
bars.forEach(b => b.classList.add('wave-active'));
|
||
});
|
||
musicaAudioPlayer.addEventListener('pause', () => {
|
||
const bars = document.querySelectorAll('#musicaWaveformVisualizer .wave-bar');
|
||
bars.forEach(b => b.classList.remove('wave-active'));
|
||
});
|
||
}
|
||
|
||
// --- GERAR MÚSICA (COMPOR 3 VERSÕES + ÁUDIO) ---
|
||
if (btnGenerateMusica) {
|
||
btnGenerateMusica.addEventListener('click', async () => {
|
||
const tema = (musicaTemaInput?.value || '').trim();
|
||
if (!tema) {
|
||
await showCustomAlert('Aviso', 'Por favor, informe a ideia ou tema da música.');
|
||
return;
|
||
}
|
||
|
||
// Coletar instrumentos selecionados
|
||
const selectedInstruments = Array.from(
|
||
document.querySelectorAll('#musicaInstrumentsContainer input[type="checkbox"]:checked')
|
||
).map(cb => cb.value).join(', ');
|
||
|
||
btnGenerateMusica.disabled = true;
|
||
if (musicaGenerateLoader) musicaGenerateLoader.style.display = 'flex';
|
||
|
||
try {
|
||
const res = await fetch('/api/musicas/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
tema,
|
||
faixaEtaria: musicaFaixaEtaria?.value,
|
||
ritmo: musicaRitmo?.value,
|
||
instrumentos: selectedInstruments || 'Violão acústico, Pandeiro e Flauta',
|
||
intensidade: musicaIntensidade?.value || 'moderada',
|
||
duracao: musicaDuracaoSelect?.value || '1min',
|
||
voz: musicaVozSelect?.value || 'mulher',
|
||
turmaId: musicaTurmaSelect?.value || null,
|
||
gerarAudio: true
|
||
})
|
||
});
|
||
|
||
const data = await res.json();
|
||
|
||
if (res.ok && data.success) {
|
||
activeMusicaData = {
|
||
titulo: data.titulo || `Música: ${tema}`,
|
||
versaoPrincipal: data.versaoPrincipal,
|
||
versaoCurta: data.versaoCurta,
|
||
versaoDancante: data.versaoDancante,
|
||
bnccCampos: data.bnccCampos,
|
||
instrumentos: data.instrumentosSugeridos || selectedInstruments,
|
||
dicas: data.dicasPedagogicas,
|
||
audioUrl: data.audioUrl,
|
||
tema: tema,
|
||
ritmo: data.ritmo || musicaRitmo?.value,
|
||
faixaEtaria: data.faixaEtaria || musicaFaixaEtaria?.value
|
||
};
|
||
|
||
// Atualizar UI
|
||
if (musicaPlaceholderText) musicaPlaceholderText.style.display = 'none';
|
||
if (musicaVersionsTabs) musicaVersionsTabs.style.display = 'flex';
|
||
if (musicaActiveContentBox) musicaActiveContentBox.style.display = 'flex';
|
||
|
||
if (musicaActiveTitle) musicaActiveTitle.textContent = activeMusicaData.titulo;
|
||
if (musicaBnccBadge) {
|
||
musicaBnccBadge.textContent = `BNCC: ${activeMusicaData.bnccCampos.split('•')[0].trim()}`;
|
||
musicaBnccBadge.style.display = 'inline-block';
|
||
}
|
||
|
||
if (musicaPedagogicalTipText && activeMusicaData.dicas) {
|
||
musicaPedagogicalTipText.textContent = activeMusicaData.dicas;
|
||
musicaPedagogicalTipBox.style.display = 'block';
|
||
}
|
||
|
||
// Se gerou áudio com MiniMax
|
||
if (data.audioUrl) {
|
||
musicaAudioPlayer.src = data.audioUrl;
|
||
if (musicaPlayerTitle) musicaPlayerTitle.textContent = activeMusicaData.titulo;
|
||
if (btnDownloadMusicaAudio) btnDownloadMusicaAudio.href = data.audioUrl;
|
||
if (musicaAudioPlayerBox) musicaAudioPlayerBox.style.display = 'flex';
|
||
} else {
|
||
if (musicaAudioPlayerBox) musicaAudioPlayerBox.style.display = 'none';
|
||
}
|
||
|
||
switchMusicaVersionTab('principal');
|
||
showToast('3 versões de música pedagógica compostas com sucesso!', 'success');
|
||
|
||
} else {
|
||
await showCustomAlert('Erro', data.error || 'Erro ao compor música.');
|
||
}
|
||
|
||
} catch (err) {
|
||
console.error('Erro ao compor música:', err);
|
||
await showCustomAlert('Erro', 'Não foi possível conectar ao servidor: ' + err.message);
|
||
} finally {
|
||
btnGenerateMusica.disabled = false;
|
||
if (musicaGenerateLoader) musicaGenerateLoader.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Copiar Letra Ativa
|
||
if (btnCopyActiveLyrics) {
|
||
btnCopyActiveLyrics.addEventListener('click', () => {
|
||
const text = musicaActiveLyrics?.textContent || '';
|
||
if (text) {
|
||
navigator.clipboard.writeText(`${activeMusicaData.titulo}\n\n${text}`);
|
||
showToast('Letra copiada para a área de transferência!', 'success');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Baixar Letra TXT
|
||
if (btnDownloadActiveTxt) {
|
||
btnDownloadActiveTxt.addEventListener('click', () => {
|
||
const text = musicaActiveLyrics?.textContent || '';
|
||
if (!text) return;
|
||
const content = `MÚSICA PEDAGÓGICA - PEDAGOGIA\n========================================\nTítulo: ${activeMusicaData.titulo}\nFaixa Etária: ${activeMusicaData.faixaEtaria}\nRitmo: ${activeMusicaData.ritmo}\nBNCC: ${activeMusicaData.bnccCampos}\nInstrumentos: ${activeMusicaData.instrumentos}\n========================================\n\n${text}\n\n----------------------------------------\nOrientação Pedagógica:\n${activeMusicaData.dicas}\n`;
|
||
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||
const link = document.createElement('a');
|
||
link.href = URL.createObjectURL(blob);
|
||
link.download = `musica_${(activeMusicaData.titulo || 'pedagogica').replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase()}.txt`;
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
link.remove();
|
||
});
|
||
}
|
||
|
||
// Exportar PDF Formatado
|
||
if (btnExportActivePdf) {
|
||
btnExportActivePdf.addEventListener('click', () => {
|
||
const lyrics = musicaActiveLyrics?.textContent || '';
|
||
if (!lyrics) return;
|
||
|
||
const printWindow = window.open('', '_blank');
|
||
printWindow.document.write(`
|
||
<html>
|
||
<head>
|
||
<title>${activeMusicaData.titulo} - PedaGog</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&family=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
body { font-family: 'Fredoka', 'Outfit', sans-serif; padding: 40px; color: #1e293b; background: #fffdfa; line-height: 1.7; }
|
||
.header-box { border-bottom: 3px dashed #a855f7; padding-bottom: 16px; margin-bottom: 24px; }
|
||
h1 { color: #7c3aed; margin: 0 0 8px 0; font-size: 2rem; }
|
||
.meta-tags { display: flex; gap: 12px; font-size: 0.9rem; color: #64748b; font-weight: 600; margin-bottom: 8px; flex-wrap: wrap; }
|
||
.meta-tag { background: #f3e8ff; color: #7e22ce; padding: 4px 10px; border-radius: 6px; }
|
||
.lyrics-box { font-size: 1.15rem; background: #f8fafc; padding: 24px; border-radius: 12px; border: 1px solid #e2e8f0; white-space: pre-wrap; margin-bottom: 24px; box-shadow: 0 2px 4px rgba(0,0,0,0.02); }
|
||
.tip-box { background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534; padding: 14px 18px; border-radius: 8px; font-size: 0.92rem; }
|
||
@media print { body { padding: 20px; } button { display: none; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div style="max-width: 750px; margin: 0 auto;">
|
||
<div class="header-box">
|
||
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
|
||
<div>
|
||
<h1>🎵 ${activeMusicaData.titulo}</h1>
|
||
<div class="meta-tags">
|
||
<span class="meta-tag">👶 ${activeMusicaData.faixaEtaria}</span>
|
||
<span class="meta-tag">🥁 ${activeMusicaData.ritmo}</span>
|
||
<span class="meta-tag">📚 ${activeMusicaData.bnccCampos.split('•')[0]}</span>
|
||
</div>
|
||
<div style="font-size: 0.85rem; color: #64748b;">🎸 Instrumentos: ${activeMusicaData.instrumentos}</div>
|
||
</div>
|
||
<button onclick="window.print()" style="background: #a855f7; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: bold; font-family: sans-serif;">🖨️ Imprimir / Salvar PDF</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="lyrics-box">${lyrics}</div>
|
||
|
||
${activeMusicaData.dicas ? `
|
||
<div class="tip-box">
|
||
💡 <strong>Vivência Pedagógica Sugerida:</strong><br>
|
||
${activeMusicaData.dicas}
|
||
</div>` : ''}
|
||
|
||
<div style="margin-top: 30px; text-align: center; font-size: 0.8rem; color: #94a3b8; border-top: 1px solid #e2e8f0; padding-top: 10px;">
|
||
Gerado por PedagogIA • Assistente Pedagógica da Professora Camila
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`);
|
||
printWindow.document.close();
|
||
});
|
||
}
|
||
|
||
// --- HISTÓRICO DE MÚSICAS ---
|
||
if (btnOpenMusicaHistory) {
|
||
btnOpenMusicaHistory.addEventListener('click', () => {
|
||
musicaHistoryModal.style.display = 'flex';
|
||
loadMusicaHistory();
|
||
});
|
||
}
|
||
|
||
if (btnCloseMusicaHistory) {
|
||
btnCloseMusicaHistory.addEventListener('click', () => {
|
||
musicaHistoryModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
musicaHistoryModal.addEventListener('click', (e) => {
|
||
if (e.target === musicaHistoryModal) {
|
||
musicaHistoryModal.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
async function loadMusicaHistory() {
|
||
const listContainer = document.getElementById('musicaHistoryList');
|
||
if (!listContainer) return;
|
||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando histórico...</p>';
|
||
|
||
try {
|
||
const response = await fetch('/api/musicas/list');
|
||
const data = await response.json();
|
||
if (data && data.length > 0) {
|
||
listContainer.innerHTML = '';
|
||
data.forEach(item => {
|
||
const date = new Date(item.created_at).toLocaleString('pt-BR');
|
||
const div = document.createElement('div');
|
||
div.className = 'history-item-card';
|
||
div.style.cssText = 'background: rgba(255,255,255,0.03); padding: 14px; border-radius: 10px; border: 1px solid var(--border-light); display: flex; flex-direction: column; gap: 8px; cursor: pointer; position: relative; transition: all 0.2s;';
|
||
div.innerHTML = `
|
||
<div style="display: flex; justify-content: space-between; align-items: flex-start; padding-right: 30px;">
|
||
<strong style="color: #c084fc; font-size: 0.95rem;">${item.faixa_etaria} • ${item.ritmo}</strong>
|
||
<span style="font-size: 0.72rem; color: var(--text-secondary);">${date}</span>
|
||
</div>
|
||
<p style="margin: 0; font-size: 0.85rem; color: var(--text-primary); opacity: 0.9;"><strong>Tema:</strong> ${item.tema}</p>
|
||
${item.audio_url ? `
|
||
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
|
||
<span style="font-size: 0.75rem; color: #10b981; font-weight: 600;">🎧 Áudio Disponível</span>
|
||
</div>
|
||
` : ''}
|
||
<button class="btn-delete-musica" data-id="${item.id}" style="position: absolute; top: 12px; right: 12px; background: transparent; border: none; color: #ef4444; cursor: pointer; font-size: 1.1rem; padding: 4px; border-radius: 4px;" title="Excluir">🗑️</button>
|
||
`;
|
||
|
||
div.addEventListener('click', async (e) => {
|
||
if (e.target.closest('.btn-delete-musica')) {
|
||
e.stopPropagation();
|
||
if (confirm('Tem certeza que deseja excluir esta música do histórico?')) {
|
||
try {
|
||
const delRes = await fetch(`/api/musicas/${item.id}`, { method: 'DELETE' });
|
||
if (delRes.ok) {
|
||
showToast('Música excluída do histórico.', 'success');
|
||
loadMusicaHistory();
|
||
}
|
||
} catch (err) {
|
||
showToast('Erro ao excluir música.', 'error');
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Carregar música clicada
|
||
try {
|
||
const res = await fetch(`/api/musicas/${item.id}`);
|
||
const detail = await res.json();
|
||
if (res.ok) {
|
||
activeMusicaData = {
|
||
titulo: `Música: ${detail.tema}`,
|
||
versaoPrincipal: detail.versao_longa || detail.musica,
|
||
versaoCurta: detail.versao_curta || detail.musica,
|
||
versaoDancante: detail.versao_dancante || detail.musica,
|
||
bnccCampos: detail.bncc || 'EI02TS02 • Traços, Sons, Cores e Formas',
|
||
instrumentos: detail.instrumentos || 'Violão e Percussão',
|
||
dicas: '',
|
||
audioUrl: detail.audio_url,
|
||
tema: detail.tema,
|
||
ritmo: detail.ritmo,
|
||
faixaEtaria: detail.faixa_etaria
|
||
};
|
||
|
||
if (musicaPlaceholderText) musicaPlaceholderText.style.display = 'none';
|
||
if (musicaVersionsTabs) musicaVersionsTabs.style.display = 'flex';
|
||
if (musicaActiveContentBox) musicaActiveContentBox.style.display = 'flex';
|
||
|
||
if (musicaActiveTitle) musicaActiveTitle.textContent = activeMusicaData.titulo;
|
||
if (detail.audio_url) {
|
||
musicaAudioPlayer.src = detail.audio_url;
|
||
if (btnDownloadMusicaAudio) btnDownloadMusicaAudio.href = detail.audio_url;
|
||
if (musicaAudioPlayerBox) musicaAudioPlayerBox.style.display = 'flex';
|
||
} else {
|
||
if (musicaAudioPlayerBox) musicaAudioPlayerBox.style.display = 'none';
|
||
}
|
||
|
||
switchMusicaVersionTab('principal');
|
||
musicaHistoryModal.style.display = 'none';
|
||
}
|
||
} catch (err) {
|
||
showToast('Erro ao carregar detalhes da música.', 'error');
|
||
}
|
||
});
|
||
|
||
listContainer.appendChild(div);
|
||
});
|
||
} else {
|
||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary); padding: 20px;">Nenhuma música gerada até o momento.</p>';
|
||
}
|
||
} catch (err) {
|
||
listContainer.innerHTML = '<p style="text-align:center; color:#ef4444;">Erro ao carregar histórico de músicas.</p>';
|
||
}
|
||
}
|
||
|
||
|
||
// ============================================================
|
||
// INVENTANDO HISTÓRIAS (ESTÚDIO PROFISSIONAL)
|
||
// ============================================================
|
||
const HISTORIA_TEMPLATES = {
|
||
arca: {
|
||
titulo: 'A Arca da Imaginação',
|
||
objeto: 'Arca Mágica de Madeira',
|
||
tema: 'As crianças encontram uma arca misteriosa no parquinho que se abre apenas quando todos dão as mãos e compartilham uma ideia criativa.',
|
||
estilo: 'Lúdico, Afetivo e Dialogado',
|
||
faixa: 'Crianças pequenas (4 anos a 5 anos e 11 meses)',
|
||
visual: 'Cartoon colorido e acolhedor'
|
||
},
|
||
horta: {
|
||
titulo: 'O Mistério da Horta Encantada',
|
||
objeto: 'Semente Brilhante de Girassol',
|
||
tema: 'A turma planta uma sementinha brilhante na horta da escola e descobre que ela cresce quando ouve canções de carinho e risadas.',
|
||
estilo: 'Contação Clássica com Suspense Leve',
|
||
faixa: 'Crianças pequenas (4 anos a 5 anos e 11 meses)',
|
||
visual: 'Aquarela suave infantil'
|
||
},
|
||
dinossauro: {
|
||
titulo: 'O Pequeno Dinossauro Amigo',
|
||
objeto: 'Pegada Colorida de Brinquedo',
|
||
tema: 'Um pequeno dinossauro verde e tímido aparece na hora do lanche procurando amigos para brincar de roda e desenhar.',
|
||
estilo: 'Lúdico, Afetivo e Dialogado',
|
||
faixa: 'Crianças bem pequenas (1 ano e 7 meses a 3 anos e 11 meses)',
|
||
visual: 'Cartoon colorido e acolhedor'
|
||
},
|
||
chuva: {
|
||
titulo: 'O Dia em que a Chuva Dançou',
|
||
objeto: 'Galochas Amarelas Saltitantes',
|
||
tema: 'Em um dia chuvoso, as gotas batem na janela fazendo música e convidam as crianças para uma dança aconchegante dentro da sala.',
|
||
estilo: 'Roda de Conversa Interativa',
|
||
faixa: 'Crianças pequenas (4 anos a 5 anos e 11 meses)',
|
||
visual: 'Aquarela suave infantil'
|
||
},
|
||
bolhas: {
|
||
titulo: 'O Reino Mágico das Bolhas de Sabão',
|
||
objeto: 'Varinha de Fazer Bolhas Gigantes',
|
||
tema: 'Cada bolha de sabão que as crianças assopram no pátio leva um desejo de abraço e alegria até as nuvens coloridas.',
|
||
estilo: 'Lúdico, Afetivo e Dialogado',
|
||
faixa: 'Bebês (0 a 1 ano e 6 meses)',
|
||
visual: 'Giz de cera e textura lúdica'
|
||
},
|
||
brinquedos: {
|
||
titulo: 'A Reunião Secreta dos Brinquedos',
|
||
objeto: 'Ursinho de Pano Guardião',
|
||
tema: 'Na hora do descanso, os brinquedos se organizam com muito capricho para esperar o acordar dos alunos com uma surpresa de blocos montados.',
|
||
estilo: 'Aventura e Descoberta na Escola',
|
||
faixa: 'Crianças pequenas (4 anos a 5 anos e 11 meses)',
|
||
visual: 'Livro infantil clássico e detalhado'
|
||
},
|
||
emocoes: {
|
||
titulo: 'O Monstrinho das Cores e dos Sentimentos',
|
||
objeto: 'Pote Brilhante das Emoções',
|
||
tema: 'Um monstrinho amigo muda de cor quando sente alegria (amarelo), calma (azul) ou saudade (rosa), ensinando a turma a expressar o que sente.',
|
||
estilo: 'Roda de Conversa Interativa',
|
||
faixa: 'Crianças pequenas (4 anos a 5 anos e 11 meses)',
|
||
visual: 'Cartoon colorido e acolhedor'
|
||
}
|
||
};
|
||
|
||
const barBtnHistoria = document.getElementById('barBtnHistoria');
|
||
const historiaModal = document.getElementById('historiaModal');
|
||
const btnCloseHistoriaModal = document.getElementById('btnCloseHistoriaModal');
|
||
const btnOpenHistoriaHistory = document.getElementById('btnOpenHistoriaHistory');
|
||
const historiaHistoryModal = document.getElementById('historiaHistoryModal');
|
||
const btnCloseHistoriaHistory = document.getElementById('btnCloseHistoriaHistory');
|
||
|
||
const historiaTemplateSelect = document.getElementById('historiaTemplateSelect');
|
||
const historiaTurmaSelect = document.getElementById('historiaTurmaSelect');
|
||
const historiaPersonagemInput = document.getElementById('historiaPersonagemInput');
|
||
const historiaObjetoMagicoInput = document.getElementById('historiaObjetoMagicoInput');
|
||
const historiaFaixaEtaria = document.getElementById('historiaFaixaEtaria');
|
||
const historiaParagrafos = document.getElementById('historiaParagrafos');
|
||
const historiaEstilo = document.getElementById('historiaEstilo');
|
||
const historiaEstiloVisual = document.getElementById('historiaEstiloVisual');
|
||
const historiaTemaInput = document.getElementById('historiaTemaInput');
|
||
const btnGenerateHistoria = document.getElementById('btnGenerateHistoria');
|
||
const historiaGenerateLoader = document.getElementById('historiaGenerateLoader');
|
||
|
||
// Elementos do Leitor e Visualizador
|
||
const historiaPlaceholderText = document.getElementById('historiaPlaceholderText');
|
||
const historiaActiveBox = document.getElementById('historiaActiveBox');
|
||
const historiaActiveTitle = document.getElementById('historiaActiveTitle');
|
||
const historiaActiveSubtitle = document.getElementById('historiaActiveSubtitle');
|
||
const historiaBnccBadge = document.getElementById('historiaBnccBadge');
|
||
|
||
// Player de Áudio
|
||
const historiaAudioPlayerBox = document.getElementById('historiaAudioPlayerBox');
|
||
const historiaAudioPlayer = document.getElementById('historiaAudioPlayer');
|
||
const btnDownloadHistoriaAudio = document.getElementById('btnDownloadHistoriaAudio');
|
||
const historiaWaveBars = document.querySelectorAll('.wave-bar-historia');
|
||
|
||
// Abas
|
||
const historiaContentTabs = document.getElementById('historiaContentTabs');
|
||
const btnTabHistoriaCenas = document.getElementById('btnTabHistoriaCenas');
|
||
const btnTabHistoriaTexto = document.getElementById('btnTabHistoriaTexto');
|
||
const btnTabHistoriaAtividades = document.getElementById('btnTabHistoriaAtividades');
|
||
const historiaCenasView = document.getElementById('historiaCenasView');
|
||
const historiaTextoView = document.getElementById('historiaTextoView');
|
||
const historiaAtividadesView = document.getElementById('historiaAtividadesView');
|
||
|
||
// Cenas Interativas
|
||
const btnPrevCena = document.getElementById('btnPrevCena');
|
||
const btnNextCena = document.getElementById('btnNextCena');
|
||
const historiaCenaIndicator = document.getElementById('historiaCenaIndicator');
|
||
const historiaCenaTitle = document.getElementById('historiaCenaTitle');
|
||
const historiaCenaText = document.getElementById('historiaCenaText');
|
||
const historiaCenaImageContainer = document.getElementById('historiaCenaImageContainer');
|
||
const historiaCenaImg = document.getElementById('historiaCenaImg');
|
||
const btnGenerateSceneImage = document.getElementById('btnGenerateSceneImage');
|
||
|
||
// BNCC e Perguntas
|
||
const historiaBnccDesc = document.getElementById('historiaBnccDesc');
|
||
const historiaPerguntasList = document.getElementById('historiaPerguntasList');
|
||
|
||
// Ações
|
||
const btnCopyActiveHistoria = document.getElementById('btnCopyActiveHistoria');
|
||
const btnDownloadActiveHistoriaTxt = document.getElementById('btnDownloadActiveHistoriaTxt');
|
||
const btnSendHistoriaToMusic = document.getElementById('btnSendHistoriaToMusic');
|
||
const btnExportActiveHistoriaPdf = document.getElementById('btnExportActiveHistoriaPdf');
|
||
|
||
let currentHistoriaData = null;
|
||
let currentSceneIndex = 0;
|
||
|
||
// Carregar turmas no select de histórias
|
||
async function loadTurmasForHistoria() {
|
||
if (!historiaTurmaSelect) return;
|
||
try {
|
||
const res = await fetch('/api/turmas');
|
||
if (res.ok) {
|
||
const turmas = await res.json();
|
||
historiaTurmaSelect.innerHTML = '<option value="">-- Selecionar Turma --</option>';
|
||
turmas.forEach(t => {
|
||
const opt = document.createElement('option');
|
||
opt.value = t.id;
|
||
opt.textContent = `${t.nome} (${t.ano_letivo || '2026'})`;
|
||
historiaTurmaSelect.appendChild(opt);
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.warn('Erro ao carregar turmas para histórias:', e);
|
||
}
|
||
}
|
||
|
||
// Mudança de template
|
||
if (historiaTemplateSelect) {
|
||
historiaTemplateSelect.addEventListener('change', () => {
|
||
const key = historiaTemplateSelect.value;
|
||
if (key && HISTORIA_TEMPLATES[key]) {
|
||
const tpl = HISTORIA_TEMPLATES[key];
|
||
historiaTemaInput.value = tpl.tema;
|
||
if (historiaObjetoMagicoInput) historiaObjetoMagicoInput.value = tpl.objeto;
|
||
if (historiaEstilo) historiaEstilo.value = tpl.estilo;
|
||
if (historiaFaixaEtaria) historiaFaixaEtaria.value = tpl.faixa;
|
||
if (historiaEstiloVisual) historiaEstiloVisual.value = tpl.visual;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Abertura do Modal
|
||
if (barBtnHistoria) {
|
||
barBtnHistoria.addEventListener('click', () => {
|
||
historiaModal.style.display = 'flex';
|
||
loadTurmasForHistoria();
|
||
if (!currentHistoriaData) {
|
||
if (historiaPlaceholderText) historiaPlaceholderText.style.display = 'block';
|
||
if (historiaActiveBox) historiaActiveBox.style.display = 'none';
|
||
if (historiaAudioPlayerBox) historiaAudioPlayerBox.style.display = 'none';
|
||
if (historiaContentTabs) historiaContentTabs.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnCloseHistoriaModal) {
|
||
historiaModal.addEventListener('click', (e) => { if (e.target === historiaModal) historiaModal.style.display = 'none'; });
|
||
btnCloseHistoriaModal.addEventListener('click', () => historiaModal.style.display = 'none');
|
||
}
|
||
|
||
// Histórico
|
||
if (btnOpenHistoriaHistory) {
|
||
btnOpenHistoriaHistory.addEventListener('click', () => {
|
||
historiaHistoryModal.style.display = 'flex';
|
||
loadHistoriaHistory();
|
||
});
|
||
}
|
||
if (btnCloseHistoriaHistory) btnCloseHistoriaHistory.addEventListener('click', () => historiaHistoryModal.style.display = 'none');
|
||
historiaHistoryModal.addEventListener('click', (e) => { if (e.target === historiaHistoryModal) historiaHistoryModal.style.display = 'none'; });
|
||
|
||
// Alternância de Abas
|
||
function switchHistoriaTab(tab) {
|
||
[btnTabHistoriaCenas, btnTabHistoriaTexto, btnTabHistoriaAtividades].forEach(b => {
|
||
if (b) {
|
||
b.classList.remove('active');
|
||
b.style.background = 'transparent';
|
||
b.style.color = 'var(--text-secondary)';
|
||
}
|
||
});
|
||
if (historiaCenasView) historiaCenasView.style.display = 'none';
|
||
if (historiaTextoView) historiaTextoView.style.display = 'none';
|
||
if (historiaAtividadesView) historiaAtividadesView.style.display = 'none';
|
||
|
||
if (tab === 'cenas') {
|
||
if (btnTabHistoriaCenas) {
|
||
btnTabHistoriaCenas.classList.add('active');
|
||
btnTabHistoriaCenas.style.background = 'rgba(16,185,129,0.15)';
|
||
btnTabHistoriaCenas.style.color = '#34d399';
|
||
}
|
||
if (historiaCenasView) historiaCenasView.style.display = 'flex';
|
||
} else if (tab === 'texto') {
|
||
if (btnTabHistoriaTexto) {
|
||
btnTabHistoriaTexto.classList.add('active');
|
||
btnTabHistoriaTexto.style.background = 'rgba(16,185,129,0.15)';
|
||
btnTabHistoriaTexto.style.color = '#34d399';
|
||
}
|
||
if (historiaTextoView) historiaTextoView.style.display = 'block';
|
||
} else if (tab === 'atividades') {
|
||
if (btnTabHistoriaAtividades) {
|
||
btnTabHistoriaAtividades.classList.add('active');
|
||
btnTabHistoriaAtividades.style.background = 'rgba(16,185,129,0.15)';
|
||
btnTabHistoriaAtividades.style.color = '#34d399';
|
||
}
|
||
if (historiaAtividadesView) historiaAtividadesView.style.display = 'flex';
|
||
}
|
||
}
|
||
|
||
if (btnTabHistoriaCenas) btnTabHistoriaCenas.addEventListener('click', () => switchHistoriaTab('cenas'));
|
||
if (btnTabHistoriaTexto) btnTabHistoriaTexto.addEventListener('click', () => switchHistoriaTab('texto'));
|
||
if (btnTabHistoriaAtividades) btnTabHistoriaAtividades.addEventListener('click', () => switchHistoriaTab('atividades'));
|
||
|
||
// Renderizar Cena Atual
|
||
function renderCurrentScene() {
|
||
if (!currentHistoriaData || !currentHistoriaData.cenas || currentHistoriaData.cenas.length === 0) return;
|
||
const total = currentHistoriaData.cenas.length;
|
||
if (currentSceneIndex < 0) currentSceneIndex = 0;
|
||
if (currentSceneIndex >= total) currentSceneIndex = total - 1;
|
||
|
||
const cena = currentHistoriaData.cenas[currentSceneIndex];
|
||
if (historiaCenaIndicator) historiaCenaIndicator.textContent = `Cena ${currentSceneIndex + 1} de ${total}`;
|
||
if (historiaCenaTitle) historiaCenaTitle.textContent = cena.titulo || `Ato ${currentSceneIndex + 1}`;
|
||
if (historiaCenaText) historiaCenaText.textContent = cena.texto || '';
|
||
|
||
// Imagem da cena
|
||
if (cena.imageUrl) {
|
||
if (historiaCenaImg) historiaCenaImg.src = cena.imageUrl;
|
||
if (historiaCenaImageContainer) historiaCenaImageContainer.style.display = 'block';
|
||
if (btnGenerateSceneImage) btnGenerateSceneImage.innerHTML = '🔄 Regenerar Ilustração';
|
||
} else {
|
||
if (historiaCenaImageContainer) historiaCenaImageContainer.style.display = 'none';
|
||
if (btnGenerateSceneImage) btnGenerateSceneImage.innerHTML = '🎨 Gerar Ilustração';
|
||
}
|
||
|
||
if (btnPrevCena) btnPrevCena.disabled = currentSceneIndex === 0;
|
||
if (btnNextCena) btnNextCena.disabled = currentSceneIndex === total - 1;
|
||
}
|
||
|
||
if (btnPrevCena) {
|
||
btnPrevCena.addEventListener('click', () => {
|
||
if (currentSceneIndex > 0) {
|
||
currentSceneIndex--;
|
||
renderCurrentScene();
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnNextCena) {
|
||
btnNextCena.addEventListener('click', () => {
|
||
if (currentHistoriaData && currentHistoriaData.cenas && currentSceneIndex < currentHistoriaData.cenas.length - 1) {
|
||
currentSceneIndex++;
|
||
renderCurrentScene();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Gerar Ilustração da Cena Atual
|
||
if (btnGenerateSceneImage) {
|
||
btnGenerateSceneImage.addEventListener('click', async () => {
|
||
if (!currentHistoriaData || !currentHistoriaData.cenas) return;
|
||
const cena = currentHistoriaData.cenas[currentSceneIndex];
|
||
if (!cena) return;
|
||
|
||
const originalBtnHtml = btnGenerateSceneImage.innerHTML;
|
||
btnGenerateSceneImage.disabled = true;
|
||
btnGenerateSceneImage.innerHTML = '⏳ Ilustrando...';
|
||
|
||
try {
|
||
const res = await fetch('/api/historias/scene-image', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
prompt: cena.promptImagem || cena.texto || 'Cute children book illustration',
|
||
estiloVisual: currentHistoriaData.estiloVisual || 'Cartoon colorido',
|
||
cenaNumero: currentSceneIndex + 1
|
||
})
|
||
});
|
||
|
||
const data = await res.json();
|
||
if (res.ok && data.imageUrl) {
|
||
cena.imageUrl = data.imageUrl;
|
||
renderCurrentScene();
|
||
showToast('Ilustração da cena criada com sucesso!', 'success');
|
||
} else {
|
||
showToast(data.error || 'Erro ao gerar ilustração.', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Erro ao conectar com gerador de ilustração.', 'error');
|
||
} finally {
|
||
btnGenerateSceneImage.disabled = false;
|
||
btnGenerateSceneImage.innerHTML = originalBtnHtml;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Gerar História Completa
|
||
if (btnGenerateHistoria) {
|
||
btnGenerateHistoria.addEventListener('click', async () => {
|
||
const tema = historiaTemaInput.value.trim();
|
||
if (!tema) {
|
||
showToast('Por favor, informe o tema ou enredo da história.', 'error');
|
||
return;
|
||
}
|
||
|
||
btnGenerateHistoria.disabled = true;
|
||
if (historiaGenerateLoader) historiaGenerateLoader.style.display = 'flex';
|
||
if (historiaPlaceholderText) historiaPlaceholderText.style.display = 'none';
|
||
|
||
try {
|
||
const response = await fetch('/api/historias/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
tema: tema,
|
||
faixaEtaria: historiaFaixaEtaria ? historiaFaixaEtaria.value : 'Crianças pequenas',
|
||
paragrafos: historiaParagrafos ? historiaParagrafos.value : '3',
|
||
estilo: historiaEstilo ? historiaEstilo.value : 'Lúdico',
|
||
estiloVisual: historiaEstiloVisual ? historiaEstiloVisual.value : 'Cartoon colorido',
|
||
personagemPrincipal: historiaPersonagemInput ? historiaPersonagemInput.value.trim() : '',
|
||
objetoMagico: historiaObjetoMagicoInput ? historiaObjetoMagicoInput.value.trim() : '',
|
||
turmaId: historiaTurmaSelect ? historiaTurmaSelect.value : null
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok && data.success) {
|
||
currentHistoriaData = data;
|
||
currentSceneIndex = 0;
|
||
|
||
// Header
|
||
if (historiaActiveTitle) historiaActiveTitle.textContent = data.titulo || 'História Mágica';
|
||
if (historiaActiveSubtitle) {
|
||
historiaActiveSubtitle.textContent = `Personagem: ${data.personagemPrincipal || 'Turma'} • ${data.cenas ? data.cenas.length : 3} Cenas • ${data.duracaoLeitura || '3 min'}`;
|
||
}
|
||
if (historiaBnccBadge) {
|
||
historiaBnccBadge.textContent = `BNCC: ${data.bnccCampos || 'EI02EF04 / EI03EF05'}`;
|
||
historiaBnccBadge.style.display = 'inline-block';
|
||
}
|
||
|
||
// Player de Áudio
|
||
if (data.audioUrl) {
|
||
if (historiaAudioPlayer) historiaAudioPlayer.src = data.audioUrl;
|
||
if (btnDownloadHistoriaAudio) btnDownloadHistoriaAudio.href = data.audioUrl;
|
||
if (historiaAudioPlayerBox) historiaAudioPlayerBox.style.display = 'flex';
|
||
|
||
if (historiaAudioPlayer) {
|
||
historiaAudioPlayer.onplay = () => historiaWaveBars.forEach(b => b.classList.add('wave-active'));
|
||
historiaAudioPlayer.onpause = () => historiaWaveBars.forEach(b => b.classList.remove('wave-active'));
|
||
historiaAudioPlayer.onended = () => historiaWaveBars.forEach(b => b.classList.remove('wave-active'));
|
||
}
|
||
} else {
|
||
if (historiaAudioPlayerBox) historiaAudioPlayerBox.style.display = 'none';
|
||
}
|
||
|
||
// Abas & Visualização
|
||
if (historiaContentTabs) historiaContentTabs.style.display = 'flex';
|
||
if (historiaActiveBox) historiaActiveBox.style.display = 'flex';
|
||
|
||
// Texto completo
|
||
if (historiaTextoView) historiaTextoView.textContent = data.historiaCompleta || '';
|
||
|
||
// Atividades e BNCC
|
||
if (historiaBnccDesc) historiaBnccDesc.textContent = data.bnccCampos || 'EI02EF04 / EI03EF05 • Escuta, Fala, Pensamento e Imaginação';
|
||
if (historiaPerguntasList) {
|
||
historiaPerguntasList.innerHTML = '';
|
||
(data.perguntasCompreensao || []).forEach(p => {
|
||
const pEl = document.createElement('div');
|
||
pEl.style.cssText = 'padding: 4px 0; border-bottom: 1px dashed rgba(255,255,255,0.05);';
|
||
pEl.textContent = p;
|
||
historiaPerguntasList.appendChild(pEl);
|
||
});
|
||
}
|
||
|
||
renderCurrentScene();
|
||
switchHistoriaTab('cenas');
|
||
showToast('História inventada com sucesso!', 'success');
|
||
} else {
|
||
showToast(data.error || 'Erro ao gerar história.', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Erro de conexão ao gerar história.', 'error');
|
||
} finally {
|
||
if (historiaGenerateLoader) historiaGenerateLoader.style.display = 'none';
|
||
btnGenerateHistoria.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Copiar História
|
||
if (btnCopyActiveHistoria) {
|
||
btnCopyActiveHistoria.addEventListener('click', () => {
|
||
if (currentHistoriaData && currentHistoriaData.historiaCompleta) {
|
||
navigator.clipboard.writeText(currentHistoriaData.historiaCompleta);
|
||
showToast('História copiada para a área de transferência!', 'success');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Baixar TXT
|
||
if (btnDownloadActiveHistoriaTxt) {
|
||
btnDownloadActiveHistoriaTxt.addEventListener('click', () => {
|
||
if (!currentHistoriaData) return;
|
||
const content = `=========================================
|
||
📖 ${currentHistoriaData.titulo || 'HISTÓRIA PEDAGÓGICA'}
|
||
=========================================
|
||
Personagem Principal: ${currentHistoriaData.personagemPrincipal || 'Turma'}
|
||
Objeto Mágico: ${currentHistoriaData.objetoMagico || 'Especial'}
|
||
BNCC: ${currentHistoriaData.bnccCampos || 'EI02EF04'}
|
||
Duração Estimada: ${currentHistoriaData.duracaoLeitura || '3 min'}
|
||
|
||
--- HISTÓRIA COMPLETA ---
|
||
${currentHistoriaData.historiaCompleta || ''}
|
||
|
||
--- PERGUNTAS PARA RODA DE CONVERSA ---
|
||
${(currentHistoriaData.perguntasCompreensao || []).join('\n')}
|
||
|
||
Gerado por PedagogIA • Assistente da Professora Camila`;
|
||
|
||
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `${(currentHistoriaData.titulo || 'historia').toLowerCase().replace(/\s+/g, '_')}.txt`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
});
|
||
}
|
||
|
||
// Musicar História
|
||
if (btnSendHistoriaToMusic) {
|
||
btnSendHistoriaToMusic.addEventListener('click', () => {
|
||
if (!currentHistoriaData) return;
|
||
const barBtnMusica = document.getElementById('barBtnMusica');
|
||
const musicaTemaInput = document.getElementById('musicaTemaInput');
|
||
historiaModal.style.display = 'none';
|
||
if (barBtnMusica) barBtnMusica.click();
|
||
if (musicaTemaInput) {
|
||
musicaTemaInput.value = `Canção sobre a história "${currentHistoriaData.titulo}": ${currentHistoriaData.objetoMagico || ''}, amizade e imaginação`;
|
||
}
|
||
showToast('História transferida para o Musicando Ideias!', 'info');
|
||
});
|
||
}
|
||
|
||
// Exportar PDF Ilustrado
|
||
if (btnExportActiveHistoriaPdf) {
|
||
btnExportActiveHistoriaPdf.addEventListener('click', () => {
|
||
if (!currentHistoriaData) return;
|
||
const printWin = window.open('', '_blank');
|
||
if (!printWin) {
|
||
showToast('Permita pop-ups para gerar o PDF.', 'warning');
|
||
return;
|
||
}
|
||
|
||
let cenasHtml = '';
|
||
(currentHistoriaData.cenas || []).forEach((c, idx) => {
|
||
cenasHtml += `
|
||
<div style="margin-bottom: 20px; padding: 15px; border-left: 4px solid #10b981; background: #f8fafc; border-radius: 0 8px 8px 0;">
|
||
<h3 style="margin-top: 0; color: #059669; font-size: 1.1rem;">${c.titulo || `Ato ${idx + 1}`}</h3>
|
||
${c.imageUrl ? `<div style="text-align: center; margin: 10px 0;"><img src="${c.imageUrl}" style="max-height: 220px; border-radius: 8px;" /></div>` : ''}
|
||
<p style="font-size: 1rem; line-height: 1.7; color: #334155; margin: 0;">${c.texto || ''}</p>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
let perguntasHtml = '';
|
||
(currentHistoriaData.perguntasCompreensao || []).forEach(p => {
|
||
perguntasHtml += `<li style="margin-bottom: 6px;">${p}</li>`;
|
||
});
|
||
|
||
printWin.document.write(`
|
||
<!DOCTYPE html>
|
||
<html lang="pt-BR">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>${currentHistoriaData.titulo || 'História Pedagógica'} - Pedagog</title>
|
||
<style>
|
||
body { font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 35px; color: #1e293b; line-height: 1.6; max-width: 800px; margin: 0 auto; }
|
||
h1 { color: #059669; margin: 0 0 6px 0; font-size: 1.8rem; }
|
||
.header-box { border-bottom: 2px solid #e2e8f0; padding-bottom: 14px; margin-bottom: 24px; }
|
||
.badge { display: inline-block; background: #d1fae5; color: #065f46; font-size: 0.8rem; padding: 4px 10px; border-radius: 6px; font-weight: bold; margin-right: 6px; }
|
||
.btn-print { background: #10b981; color: white; border: none; padding: 10px 20px; border-radius: 8px; font-weight: bold; cursor: pointer; float: right; font-size: 0.95rem; }
|
||
@media print { .btn-print { display: none; } body { padding: 0; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<button class="btn-print" onclick="window.print()">🖨️ Imprimir / Salvar PDF</button>
|
||
<div class="header-box">
|
||
<h1>📖 ${currentHistoriaData.titulo || 'História Pedagógica'}</h1>
|
||
<div style="margin-top: 8px;">
|
||
<span class="badge">Personagem: ${currentHistoriaData.personagemPrincipal || 'Turma'}</span>
|
||
<span class="badge">Objeto: ${currentHistoriaData.objetoMagico || 'Especial'}</span>
|
||
<span class="badge">BNCC: ${currentHistoriaData.bnccCampos || 'EI02EF04'}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<h2 style="color: #0f172a; font-size: 1.2rem; border-bottom: 1px solid #cbd5e1; padding-bottom: 6px;">🎬 Cenas da História</h2>
|
||
${cenasHtml}
|
||
|
||
<div style="margin-top: 30px; padding: 18px; background: #f1f5f9; border-radius: 8px;">
|
||
<h3 style="margin-top: 0; color: #0f172a; font-size: 1.05rem;">🧩 Roda de Conversa & Atividades</h3>
|
||
<ul style="margin: 0; padding-left: 20px; color: #475569; font-size: 0.95rem;">
|
||
${perguntasHtml}
|
||
</ul>
|
||
</div>
|
||
|
||
<div style="margin-top: 40px; text-align: center; font-size: 0.8rem; color: #94a3b8; border-top: 1px solid #e2e8f0; padding-top: 12px;">
|
||
Pedagog • Estúdio de Literatura Infantil da Professora Camila Martella Gasparini Reifonas
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`);
|
||
printWin.document.close();
|
||
});
|
||
}
|
||
|
||
// Carregar Histórico
|
||
async function loadHistoriaHistory() {
|
||
const listContainer = document.getElementById('historiaHistoryList');
|
||
if (!listContainer) return;
|
||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando histórias...</p>';
|
||
try {
|
||
const response = await fetch('/api/historias/list');
|
||
const data = await response.json();
|
||
if (data && data.length > 0) {
|
||
listContainer.innerHTML = '';
|
||
data.forEach(item => {
|
||
const date = new Date(item.created_at).toLocaleString('pt-BR');
|
||
const div = document.createElement('div');
|
||
div.className = 'history-item-card';
|
||
div.style.cssText = 'background: rgba(255,255,255,0.02); padding: 14px; border-radius: 8px; border: 1px solid var(--border-light); display: flex; flex-direction: column; gap: 6px; cursor: pointer; position: relative;';
|
||
div.innerHTML = `
|
||
<div style="display: flex; justify-content: space-between; align-items: flex-start; padding-right: 30px;">
|
||
<strong style="color: #34d399; font-size: 0.95rem;">${item.titulo || item.tema || 'História'}</strong>
|
||
<span style="font-size: 0.72rem; color: var(--text-secondary);">${date}</span>
|
||
</div>
|
||
<p style="margin: 0; font-size: 0.82rem; color: var(--text-primary); opacity: 0.9;">Tema: ${item.tema}</p>
|
||
<div style="display: flex; gap: 8px; font-size: 0.72rem; color: var(--text-secondary);">
|
||
<span>👶 ${item.faixa_etaria}</span>
|
||
${item.personagem_principal ? `<span>• 👤 ${item.personagem_principal}</span>` : ''}
|
||
${item.audio_url ? `<span>• 🎙️ Áudio MP3</span>` : ''}
|
||
</div>
|
||
<button class="btn-delete-historia" data-id="${item.id}" style="position: absolute; top: 12px; right: 12px; background: transparent; border: none; color: #ef4444; cursor: pointer; font-size: 1.1rem; padding: 4px; border-radius: 4px;" title="Excluir">🗑️</button>
|
||
`;
|
||
div.addEventListener('click', async (e) => {
|
||
if (e.target.closest('.btn-delete-historia')) {
|
||
e.stopPropagation();
|
||
if (confirm('Tem certeza que deseja excluir esta história?')) {
|
||
try {
|
||
const delRes = await fetch(`/api/historias/${item.id}`, { method: 'DELETE' });
|
||
if (delRes.ok) { showToast('História excluída.', 'success'); loadHistoriaHistory(); }
|
||
} catch (err) { showToast('Erro ao excluir.', 'error'); }
|
||
}
|
||
return;
|
||
}
|
||
try {
|
||
const res = await fetch(`/api/historias/${item.id}`);
|
||
const detail = await res.json();
|
||
if (res.ok) {
|
||
currentHistoriaData = {
|
||
id: detail.id,
|
||
titulo: detail.titulo || `História: ${detail.tema}`,
|
||
personagemPrincipal: detail.personagem_principal || '',
|
||
objetoMagico: detail.objeto_magico || '',
|
||
historiaCompleta: detail.historia || '',
|
||
cenas: typeof detail.cenas === 'string' ? JSON.parse(detail.cenas) : (detail.cenas || []),
|
||
perguntasCompreensao: typeof detail.perguntas_compreensao === 'string' ? JSON.parse(detail.perguntas_compreensao) : (detail.perguntas_compreensao || []),
|
||
bnccCampos: detail.bncc || 'EI02EF04 / EI03EF05',
|
||
duracaoLeitura: detail.duracao_leitura || '3 min',
|
||
complexidade: detail.complexidade || 'Lúdica',
|
||
audioUrl: detail.audio_url || null,
|
||
estiloVisual: detail.estilo_visual || 'Cartoon colorido'
|
||
};
|
||
currentSceneIndex = 0;
|
||
|
||
if (historiaActiveTitle) historiaActiveTitle.textContent = currentHistoriaData.titulo;
|
||
if (historiaActiveSubtitle) {
|
||
historiaActiveSubtitle.textContent = `Personagem: ${currentHistoriaData.personagemPrincipal || 'Turma'} • ${currentHistoriaData.cenas ? currentHistoriaData.cenas.length : 3} Cenas`;
|
||
}
|
||
if (historiaBnccBadge) {
|
||
historiaBnccBadge.textContent = `BNCC: ${currentHistoriaData.bnccCampos}`;
|
||
historiaBnccBadge.style.display = 'inline-block';
|
||
}
|
||
|
||
if (currentHistoriaData.audioUrl) {
|
||
if (historiaAudioPlayer) historiaAudioPlayer.src = currentHistoriaData.audioUrl;
|
||
if (btnDownloadHistoriaAudio) btnDownloadHistoriaAudio.href = currentHistoriaData.audioUrl;
|
||
if (historiaAudioPlayerBox) historiaAudioPlayerBox.style.display = 'flex';
|
||
} else {
|
||
if (historiaAudioPlayerBox) historiaAudioPlayerBox.style.display = 'none';
|
||
}
|
||
|
||
if (historiaContentTabs) historiaContentTabs.style.display = 'flex';
|
||
if (historiaActiveBox) historiaActiveBox.style.display = 'flex';
|
||
if (historiaPlaceholderText) historiaPlaceholderText.style.display = 'none';
|
||
|
||
if (historiaTextoView) historiaTextoView.textContent = currentHistoriaData.historiaCompleta;
|
||
if (historiaBnccDesc) historiaBnccDesc.textContent = currentHistoriaData.bnccCampos;
|
||
if (historiaPerguntasList) {
|
||
historiaPerguntasList.innerHTML = '';
|
||
(currentHistoriaData.perguntasCompreensao || []).forEach(p => {
|
||
const pEl = document.createElement('div');
|
||
pEl.style.cssText = 'padding: 4px 0; border-bottom: 1px dashed rgba(255,255,255,0.05);';
|
||
pEl.textContent = p;
|
||
historiaPerguntasList.appendChild(pEl);
|
||
});
|
||
}
|
||
|
||
renderCurrentScene();
|
||
switchHistoriaTab('cenas');
|
||
historiaHistoryModal.style.display = 'none';
|
||
}
|
||
} catch (err) { showToast('Erro ao carregar história.', 'error'); }
|
||
});
|
||
listContainer.appendChild(div);
|
||
});
|
||
} else {
|
||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Nenhuma história salva ainda.</p>';
|
||
}
|
||
} catch (err) {
|
||
listContainer.innerHTML = '<p style="text-align:center; color:#ef4444;">Erro ao carregar histórico de histórias.</p>';
|
||
}
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', initApp);
|
||
} else {
|
||
initApp();
|
||
}
|
||
// ==========================================================================
|
||
// PLANEJAMENTO MIND LAB
|
||
// ==========================================================================
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const barBtnMindLab = document.getElementById('barBtnMindLab');
|
||
const mindLabModal = document.getElementById('mindLabModal');
|
||
const btnCloseMindLabModal = document.getElementById('btnCloseMindLabModal');
|
||
const mindLabGamesGrid = document.getElementById('mindLabGamesGrid');
|
||
const mindLabOutroJogoInput = document.getElementById('mindLabOutroJogoInput');
|
||
const btnGenerateMindLab = document.getElementById('btnGenerateMindLab');
|
||
|
||
const mindLabJogos = {
|
||
infantil: [
|
||
"Gato e Rato", "Cães e Gatos", "Dominó de Cores", "Quebra-cabeças", "Jogos de Encaixe", "Castelo Lógico", "Safari", "Outro"
|
||
],
|
||
fund1_inicio: [
|
||
"Mancala", "Quoridor", "Hora do Rush", "Bloqueio", "Lig 4", "Pinguins Numa Fria", "Encruzilhada", "Lince", "Outro"
|
||
],
|
||
fund1_fim: [
|
||
"Abalone", "Quarto", "Octógono Fantástico", "Sudoku", "Damas", "Resta Um", "Xadrez Chinês", "Cilada", "Outro"
|
||
],
|
||
fund2: [
|
||
"Xadrez", "Mastermind (Senha)", "Abalone Avançado", "Go", "Hex", "Reversi (Othello)", "Blokus", "Outro"
|
||
]
|
||
};
|
||
|
||
function renderMindLabGames(faixa) {
|
||
mindLabGamesGrid.innerHTML = '';
|
||
const jogos = mindLabJogos[faixa] || [];
|
||
jogos.forEach((jogo, index) => {
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = `btn-music-option btn-mindlab-jogo ${index === 0 ? 'active' : ''}`;
|
||
btn.dataset.jogo = jogo;
|
||
btn.textContent = jogo === 'Outro' ? '✏️ Outro...' : `🎲 ${jogo}`;
|
||
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.btn-mindlab-jogo').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
if (jogo === 'Outro') {
|
||
mindLabOutroJogoInput.style.display = 'block';
|
||
mindLabOutroJogoInput.focus();
|
||
} else {
|
||
mindLabOutroJogoInput.style.display = 'none';
|
||
}
|
||
});
|
||
mindLabGamesGrid.appendChild(btn);
|
||
});
|
||
mindLabOutroJogoInput.style.display = 'none';
|
||
}
|
||
|
||
// Setup Faixa Etária
|
||
document.querySelectorAll('.btn-mindlab-faixa').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.btn-mindlab-faixa').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
renderMindLabGames(btn.dataset.faixa);
|
||
});
|
||
});
|
||
|
||
// Setup Método e Foco
|
||
document.querySelectorAll('.btn-mindlab-metodo').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.btn-mindlab-metodo').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.btn-mindlab-foco').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.btn-mindlab-foco').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
});
|
||
});
|
||
|
||
if (barBtnMindLab) {
|
||
barBtnMindLab.addEventListener('click', () => {
|
||
mindLabModal.style.display = 'flex';
|
||
renderMindLabGames('infantil');
|
||
document.getElementById('mindLabTemaLivreInput').value = '';
|
||
});
|
||
}
|
||
|
||
if (btnCloseMindLabModal) {
|
||
btnCloseMindLabModal.addEventListener('click', () => {
|
||
mindLabModal.style.display = 'none';
|
||
});
|
||
mindLabModal.addEventListener('click', (e) => {
|
||
if (e.target === mindLabModal) mindLabModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
if (btnGenerateMindLab) {
|
||
btnGenerateMindLab.addEventListener('click', async () => {
|
||
const activeFaixa = document.querySelector('.btn-mindlab-faixa.active');
|
||
const activeJogoBtn = document.querySelector('.btn-mindlab-jogo.active');
|
||
const activeMetodo = document.querySelector('.btn-mindlab-metodo.active');
|
||
const activeFoco = document.querySelector('.btn-mindlab-foco.active');
|
||
|
||
const faixaStr = activeFaixa ? activeFaixa.textContent.replace('🧸 ', '').replace('🎒 ', '').replace('📚 ', '').replace('🎓 ', '') : '';
|
||
let jogoStr = activeJogoBtn ? activeJogoBtn.dataset.jogo : '';
|
||
if (jogoStr === 'Outro') {
|
||
jogoStr = mindLabOutroJogoInput.value.trim() || 'Jogo não especificado';
|
||
}
|
||
|
||
const metodoStr = activeMetodo ? activeMetodo.dataset.metodo : '';
|
||
const focoStr = activeFoco ? activeFoco.dataset.foco : '';
|
||
const duracaoStr = document.getElementById('mindLabDuracaoSelect').value;
|
||
const temaLivre = document.getElementById('mindLabTemaLivreInput').value.trim();
|
||
|
||
const btnG = document.getElementById('btnGenerateMindLab');
|
||
const loader = document.getElementById('mindLabGenerateLoader');
|
||
const resultArea = document.getElementById('mindLabResultArea');
|
||
const output = document.getElementById('mindLabOutput');
|
||
|
||
btnG.style.display = 'none';
|
||
loader.style.display = 'flex';
|
||
resultArea.style.display = 'none';
|
||
|
||
try {
|
||
const res = await fetch('/api/mindlab/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` },
|
||
body: JSON.stringify({ faixa_etaria: faixaStr, jogo: jogoStr, metodo: metodoStr, foco: focoStr, duracao: duracaoStr, tema: temaLivre })
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || 'Erro na requisição');
|
||
|
||
output.innerHTML = typeof marked !== 'undefined' ? marked.parse(data.planejamento) : data.planejamento;
|
||
output.dataset.raw = data.planejamento;
|
||
resultArea.style.display = 'block';
|
||
btnG.style.display = 'flex';
|
||
btnG.innerHTML = '🧠 Criar Novo Planejamento';
|
||
} catch (err) {
|
||
alert('Erro ao gerar: ' + err.message);
|
||
btnG.style.display = 'flex';
|
||
} finally {
|
||
loader.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
const btnCopyMindLab = document.getElementById('btnCopyMindLab');
|
||
if (btnCopyMindLab) {
|
||
btnCopyMindLab.addEventListener('click', () => {
|
||
const output = document.getElementById('mindLabOutput');
|
||
if (output && output.dataset.raw) {
|
||
navigator.clipboard.writeText(output.dataset.raw);
|
||
btnCopyMindLab.innerHTML = '✅ Copiado!';
|
||
setTimeout(() => {
|
||
btnCopyMindLab.innerHTML = '📋 Copiar';
|
||
}, 2000);
|
||
}
|
||
});
|
||
}
|
||
|
||
const btnPdfMindLab = document.getElementById('btnPdfMindLab');
|
||
if (btnPdfMindLab) {
|
||
btnPdfMindLab.addEventListener('click', () => {
|
||
const output = document.getElementById('mindLabOutput');
|
||
if (output && output.dataset.raw && window.jspdf) {
|
||
const { jsPDF } = window.jspdf;
|
||
const doc = new jsPDF();
|
||
doc.setFontSize(12);
|
||
doc.setFont("helvetica", "normal");
|
||
const lines = doc.splitTextToSize(output.dataset.raw.replace(/[#*]/g, ''), 180);
|
||
doc.text(lines, 15, 20);
|
||
doc.save('Planejamento_Mind_Lab.pdf');
|
||
} else {
|
||
alert('Carregando biblioteca PDF ou nenhum conteúdo para salvar.');
|
||
}
|
||
});
|
||
}
|
||
|
||
const btnOpenMindLabHistory = document.getElementById('btnOpenMindLabHistory');
|
||
const mindLabHistoryModal = document.getElementById('mindLabHistoryModal');
|
||
const btnCloseMindLabHistory = document.getElementById('btnCloseMindLabHistory');
|
||
const mindLabHistoryList = document.getElementById('mindLabHistoryList');
|
||
|
||
if (btnOpenMindLabHistory) {
|
||
btnOpenMindLabHistory.addEventListener('click', async () => {
|
||
mindLabHistoryModal.style.display = 'flex';
|
||
mindLabHistoryList.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">Carregando histórico...</div>';
|
||
try {
|
||
const res = await fetch('/api/mindlab/list', {
|
||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||
});
|
||
const data = await res.json();
|
||
mindLabHistoryList.innerHTML = '';
|
||
if (data.length === 0) {
|
||
mindLabHistoryList.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">Nenhum planejamento salvo ainda.</div>';
|
||
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 = `
|
||
<div>
|
||
<div style="font-weight: 600; font-size: 0.95rem; color: var(--text-primary);">🎲 ${item.jogo} - ${item.faixa_etaria}</div>
|
||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-top: 4px;">🎯 ${item.foco}</div>
|
||
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 4px;">${new Date(item.created_at).toLocaleString()}</div>
|
||
</div>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button class="btn-ver-mindlab btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.85rem; border-radius: 6px;">Ver</button>
|
||
<button class="btn-del-mindlab btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.85rem; border-radius: 6px; color: #ef4444; border-color: rgba(239, 68, 68, 0.3);">🗑️</button>
|
||
</div>
|
||
`;
|
||
mindLabHistoryList.appendChild(div);
|
||
});
|
||
|
||
document.querySelectorAll('.btn-ver-mindlab').forEach(btn => {
|
||
btn.addEventListener('click', async (e) => {
|
||
const id = e.currentTarget.dataset.id;
|
||
try {
|
||
const res = await fetch(`/api/mindlab/${id}`, {
|
||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||
});
|
||
const itemData = await res.json();
|
||
|
||
document.getElementById('mindLabOutput').innerHTML = typeof marked !== 'undefined' ? marked.parse(itemData.planejamento) : itemData.planejamento;
|
||
document.getElementById('mindLabOutput').dataset.raw = itemData.planejamento;
|
||
document.getElementById('mindLabResultArea').style.display = 'block';
|
||
|
||
mindLabHistoryModal.style.display = 'none';
|
||
document.getElementById('btnGenerateMindLab').style.display = 'flex';
|
||
} catch (err) { alert('Erro ao carregar planejamento.'); }
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.btn-del-mindlab').forEach(btn => {
|
||
btn.addEventListener('click', async (e) => {
|
||
if(!confirm('Tem certeza que deseja apagar este planejamento?')) return;
|
||
const id = e.currentTarget.dataset.id;
|
||
try {
|
||
await fetch(`/api/mindlab/${id}`, {
|
||
method: 'DELETE',
|
||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||
});
|
||
e.currentTarget.closest('div[style*="var(--bg-tertiary)"]').remove();
|
||
} catch (err) { alert('Erro ao deletar.'); }
|
||
});
|
||
});
|
||
|
||
} catch (err) {
|
||
mindLabHistoryList.innerHTML = '<div style="color: #ef4444; padding: 20px;">Erro ao carregar histórico.</div>';
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnCloseMindLabHistory) {
|
||
btnCloseMindLabHistory.addEventListener('click', () => {
|
||
mindLabHistoryModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
// ============================================================
|
||
// CRIAR CARTAZES
|
||
// ============================================================
|
||
const barBtnCartazes = document.getElementById('barBtnCartazes');
|
||
const cartazesModal = document.getElementById('cartazesModal');
|
||
const btnCloseCartazesModal = document.getElementById('btnCloseCartazesModal');
|
||
|
||
const tabCartazesCriar = document.getElementById('tabCartazesCriar');
|
||
const tabCartazesHistorico = document.getElementById('tabCartazesHistorico');
|
||
const panelCartazesCriar = document.getElementById('panelCartazesCriar');
|
||
const panelCartazesHistorico = document.getElementById('panelCartazesHistorico');
|
||
|
||
const cartazTituloInput = document.getElementById('cartazTituloInput');
|
||
const cartazSubtituloInput = document.getElementById('cartazSubtituloInput');
|
||
const cartazCardsInput = document.getElementById('cartazCardsInput');
|
||
const cartazRodapeInput = document.getElementById('cartazRodapeInput');
|
||
const cartazTemplateSelect = document.getElementById('cartazTemplateSelect');
|
||
const cartazTurmaSelect = document.getElementById('cartazTurmaSelect');
|
||
const btnCartazResetTemplate = document.getElementById('btnCartazResetTemplate');
|
||
|
||
const cartazBgColor = document.getElementById('cartazBgColor');
|
||
const cartazTextColor = document.getElementById('cartazTextColor');
|
||
const cartazFontSelect = document.getElementById('cartazFontSelect');
|
||
const cartazBorderSelect = document.getElementById('cartazBorderSelect');
|
||
const cartazPromptImagem = document.getElementById('cartazPromptImagem');
|
||
const btnGenerateCartazImage = document.getElementById('btnGenerateCartazImage');
|
||
const btnRemoveCartazImage = document.getElementById('btnRemoveCartazImage');
|
||
const btnGenerateCartaz = document.getElementById('btnGenerateCartaz');
|
||
const cartazGenerateLoader = document.getElementById('cartazGenerateLoader');
|
||
|
||
const btnExportCartazPNG = document.getElementById('btnExportCartazPNG');
|
||
const btnExportCartazPDF = document.getElementById('btnExportCartazPDF');
|
||
const btnEditCartazToggle = document.getElementById('btnEditCartazToggle');
|
||
const btnPrintCartaz = document.getElementById('btnPrintCartaz');
|
||
const cartazEditArea = document.getElementById('cartazEditArea');
|
||
const cartazHtmlEditor = document.getElementById('cartazHtmlEditor');
|
||
const btnUpdateCartazPreview = document.getElementById('btnUpdateCartazPreview');
|
||
|
||
const cartazPrintableArea = document.getElementById('cartazPrintableArea');
|
||
const cartazPreviewTitle = document.getElementById('cartazPreviewTitle');
|
||
const cartazPreviewContent = document.getElementById('cartazPreviewContent');
|
||
const cartazPreviewImageContainer = document.getElementById('cartazPreviewImageContainer');
|
||
const cartazPreviewImage = document.getElementById('cartazPreviewImage');
|
||
|
||
const cartazesHistoryContainer = document.getElementById('cartazesHistoryContainer');
|
||
const cartazesNoData = document.getElementById('cartazesNoData');
|
||
|
||
let activeCartazData = null;
|
||
|
||
// DICIONÁRIO DE TEMPLATES PEDAGÓGICOS PRÉ-CONFIGURADOS
|
||
const CARTAZ_TEMPLATES = {
|
||
combinados: {
|
||
titulo: "🌟 Combinados da Nossa Turma",
|
||
subtitulo: "Educação Infantil • Convivência e Respeito",
|
||
cards: [
|
||
"🤝 Cuidamos dos nossos amigos com carinho",
|
||
"🧸 Guardamos os brinquedos nos cantos certos",
|
||
"🤫 Falamos baixinho na roda de conversa",
|
||
"🧼 Lavamos as mãos antes do lanche",
|
||
"👂 Ouvimos a professora e os colegas com atenção"
|
||
],
|
||
rodape: "Educando com afeto, empatia e respeito mútuo",
|
||
bg: "#fff9eb",
|
||
text: "#2d3748",
|
||
font: "'Fredoka', sans-serif",
|
||
prompt: "Children holding hands happily in a colorful kindergarten classroom, cute 3D Pixar illustration, bright warm colors, no text"
|
||
},
|
||
rotina: {
|
||
titulo: "🍏 Rotina Diária & Lanche Saudável",
|
||
subtitulo: "Desenvolvimento de Autonomia e Hábitos Saudáveis",
|
||
cards: [
|
||
"🌅 Roda de Acolhida e Canções do Bom Dia",
|
||
"🧼 Higiene e Lavagem Cuidadosa das Mãos",
|
||
"🍎 Hora do Lanche: Servir-se sem desperdícios",
|
||
"🌳 Vivências no Parque, Areia e Natureza",
|
||
"😴 Roda de Histórias, Livros e Relaxamento"
|
||
],
|
||
rodape: "Promovendo a autonomia e o autocuidado no cotidiano",
|
||
bg: "#f0fdf4",
|
||
text: "#166534",
|
||
font: "'Fredoka', sans-serif",
|
||
prompt: "Cute children having healthy fruit snack at kindergarten table, friendly cartoon vector style, bright colors, no text"
|
||
},
|
||
aniversarios: {
|
||
titulo: "🎂 Mural dos Aniversariantes",
|
||
subtitulo: "Celebrando a vida e o crescimento das nossas crianças!",
|
||
cards: [
|
||
"🎈 Que cada dia seja repleto de brincadeiras!",
|
||
"🎉 Parabéns aos aniversariantes do mês!",
|
||
"🌟 Muita saúde, descobertas e sorrisos!"
|
||
],
|
||
rodape: "Com todo carinho da Professora e dos Colegas",
|
||
bg: "#fdf2f8",
|
||
text: "#9d174d",
|
||
font: "'Fredoka', sans-serif",
|
||
prompt: "Festive colorful birthday balloons and confetti with smiling animal mascots, cute 3D Pixar illustration, no text"
|
||
},
|
||
brinquedos: {
|
||
titulo: "🧸 Organização dos Brinquedos e Cantos",
|
||
subtitulo: "Nosso espaço escolar é de todos nós!",
|
||
cards: [
|
||
"🧩 Blocos de montar: Caixa Azul",
|
||
"🚗 Carrinhos e transportes: Caixa Verde",
|
||
"📚 Livros de histórias: Estante Baixa",
|
||
"🎨 Tintas e massinhas: Armário de Artes"
|
||
],
|
||
rodape: "Cuidar dos materiais é um ato de carinho e respeito",
|
||
bg: "#eff6ff",
|
||
text: "#1e40af",
|
||
font: "'Fredoka', sans-serif",
|
||
prompt: "Cute organized classroom toy shelf with colorful wooden blocks and teddy bears, cartoon style, no text"
|
||
},
|
||
bncc_eu_outro: {
|
||
titulo: "🤝 O Eu, o Outro e o Nós",
|
||
subtitulo: "Campo de Experiência BNCC • EI02EO / EI03EO",
|
||
cards: [
|
||
"🌟 Reconhecer e valorizar suas próprias qualidades",
|
||
"💖 Desenvolver empatia e respeito aos sentimentos alheios",
|
||
"🗣️ Resolver pequenos conflitos através do diálogo",
|
||
"🎨 Compartilhar materiais e respeitar a vez dos amigos"
|
||
],
|
||
rodape: "BNCC Educação Infantil • Identidade, Autonomia e Convivência",
|
||
bg: "#eff6ff",
|
||
text: "#1e40af",
|
||
font: "'Outfit', sans-serif",
|
||
prompt: "Children of diverse backgrounds smiling together and sharing toys, gentle watercolor illustration, no text"
|
||
},
|
||
bncc_corpo: {
|
||
titulo: "🏃 Corpo, Gestos e Movimentos",
|
||
subtitulo: "Campo de Experiência BNCC • EI02CG / EI03CG",
|
||
cards: [
|
||
"🤸 Circuitos motores, equilíbrio, saltos e giros",
|
||
"🎶 Cantigas de roda e expressão corporal rítmica",
|
||
"🚲 Deslocamentos espaciais e controle corporal",
|
||
"⚽ Gincanas cooperativas e respeito a regras"
|
||
],
|
||
rodape: "BNCC Educação Infantil • Corporeidade, Saúde e Movimento",
|
||
bg: "#fdf2f8",
|
||
text: "#9d174d",
|
||
font: "'Fredoka', sans-serif",
|
||
prompt: "Cute children running, jumping, and playing ring around the rosie outdoors, lively vibrant illustration, no text"
|
||
},
|
||
bncc_tracos: {
|
||
titulo: "🎨 Traços, Sons, Cores e Formas",
|
||
subtitulo: "Campo de Experiência BNCC • EI02TS / EI03TS",
|
||
cards: [
|
||
"🖌️ Pinturas com texturas e tintas naturais",
|
||
"🥁 Exploração de instrumentos musicais e ritmos",
|
||
"🖍️ Desenhos livres de observação e imaginação",
|
||
"🎭 Esculturas com massinha, argila e sucatas"
|
||
],
|
||
rodape: "BNCC Educação Infantil • Sensibilidade, Expressão e Arte",
|
||
bg: "#fff9eb",
|
||
text: "#b45309",
|
||
font: "'Caveat', cursive",
|
||
prompt: "Children happily painting on big canvases with colorful paint splashes, cute art studio, no text"
|
||
},
|
||
bncc_escuta: {
|
||
titulo: "📖 Escuta, Fala, Pensamento e Imaginação",
|
||
subtitulo: "Campo de Experiência BNCC • EI02EF / EI03EF",
|
||
cards: [
|
||
"📚 Roda de contação e manuseio de livros infantis",
|
||
"🗣️ Expressão oral de ideias, sentimentos e histórias",
|
||
"🎵 Cantigas, parlendas, rimas e trava-línguas",
|
||
"✍️ Contato lúdico com a função social da escrita"
|
||
],
|
||
rodape: "BNCC Educação Infantil • Linguagem e Letramento Inicial",
|
||
bg: "#f0fdf4",
|
||
text: "#15803d",
|
||
font: "'Fredoka', sans-serif",
|
||
prompt: "Teacher reading an open fairy tale storybook to cozy circle of children, magical soft glow, Pixar 3D, no text"
|
||
},
|
||
bncc_espacos: {
|
||
titulo: "🔍 Espaços, Tempos, Quantidades e Relações",
|
||
subtitulo: "Campo de Experiência BNCC • EI02ET / EI03ET",
|
||
cards: [
|
||
"🌱 Observação do plantio na horta e natureza",
|
||
"🔢 Contagem lúdica com elementos concretos",
|
||
"📐 Identificação de formas geométricas no ambiente",
|
||
"⏳ Noções de tempo: ontem, hoje, amanhã e rotina"
|
||
],
|
||
rodape: "BNCC Educação Infantil • Curiosidade e Investigação Científica",
|
||
bg: "#eff6ff",
|
||
text: "#1e3a8a",
|
||
font: "'Outfit', sans-serif",
|
||
prompt: "Kids exploring green garden with magnifying glass and planting flowers, vibrant clean illustration, no text"
|
||
},
|
||
mindlab_regras: {
|
||
titulo: "♟️ Cantinho dos Jogos Mind Lab",
|
||
subtitulo: "Metodologia de Jogos & Desenvolvimento Socioemocional",
|
||
cards: [
|
||
"🎯 Pensar antes de agir e planejar sua jogada",
|
||
"🤝 Saber ganhar e saber perder com elegância",
|
||
"⏳ Aguardar a sua vez com calma e autocontrole",
|
||
"💡 Aprender com as tentativas e criar novas estratégias"
|
||
],
|
||
rodape: "Mind Lab Brasil • Jogos Pedagógicos e Habilidades do Século XXI",
|
||
bg: "#f8fafc",
|
||
text: "#1e293b",
|
||
font: "'Outfit', sans-serif",
|
||
prompt: "Children happily playing a wooden tabletop board game together, focus and teamwork, 3D Pixar, no text"
|
||
},
|
||
mindlab_cooperacao: {
|
||
titulo: "🧩 Desafios de Raciocínio & Cooperação",
|
||
subtitulo: "Metodologia Mind Lab • Resolução de Problemas",
|
||
cards: [
|
||
"👥 O trabalho em equipe nos leva mais longe",
|
||
"🔍 Escuta atenta das ideias dos colegas",
|
||
"🚀 Cada desafio é uma oportunidade de aprender"
|
||
],
|
||
rodape: "Desenvolvendo liderança, empatia e tomada de decisão",
|
||
bg: "#f0fdf4",
|
||
text: "#166534",
|
||
font: "'Fredoka', sans-serif",
|
||
prompt: "Children putting together giant colorful puzzle pieces together, cooperation, joyful 3D illustration, no text"
|
||
},
|
||
aviso_pais: {
|
||
titulo: "📌 Recado Importante para as Famílias",
|
||
subtitulo: "Comunicação Escola & Comunidade Escolar",
|
||
cards: [
|
||
"📅 Próxima Reunião Pedagógica: Sexta-feira às 17h",
|
||
"🎒 Trazer uma muda de roupa extra identificada",
|
||
"👟 Uso de calçado confortável para o dia de parque",
|
||
"💧 Manter a garrafinha de água individual na mochila"
|
||
],
|
||
rodape: "A parceria entre escola e família é fundamental para o sucesso das crianças!",
|
||
bg: "#fff9eb",
|
||
text: "#1e293b",
|
||
font: "'Outfit', sans-serif",
|
||
prompt: "Friendly cartoon school bulletin board with colorful notes and cheerful mascot, clean vector, no text"
|
||
},
|
||
mostra_cultural: {
|
||
titulo: "🎉 Convite: Mostra Pedagógica & Cultural",
|
||
subtitulo: "Venha prestigiar as produções e vivências dos nossos pequenos!",
|
||
cards: [
|
||
"🎨 Exposição dos Painéis de Arte e Garatujas",
|
||
"📸 Linha do Tempo das Descobertas do Semestre",
|
||
"🎶 Apresentação Musical e Parlendas da Turma",
|
||
"🌿 Visitação guiada à Horta e Projetos Coletivos"
|
||
],
|
||
rodape: "Contamos com a presença de todas as famílias!",
|
||
bg: "#fdf2f8",
|
||
text: "#831843",
|
||
font: "'Playfair Display', serif",
|
||
prompt: "Joyful school festival decoration with colorful bunting banners and children artworks, vibrant light, no text"
|
||
}
|
||
};
|
||
|
||
|
||
|
||
// Switch Tab
|
||
function switchCartazTab(tab) {
|
||
if (tab === 'criar') {
|
||
tabCartazesCriar.classList.add('active');
|
||
tabCartazesHistorico.classList.remove('active');
|
||
panelCartazesCriar.style.display = 'flex';
|
||
panelCartazesHistorico.style.display = 'none';
|
||
tabCartazesCriar.style.color = 'var(--text-primary)';
|
||
tabCartazesHistorico.style.color = 'var(--text-secondary)';
|
||
} else {
|
||
tabCartazesCriar.classList.remove('active');
|
||
tabCartazesHistorico.classList.add('active');
|
||
panelCartazesCriar.style.display = 'none';
|
||
panelCartazesHistorico.style.display = 'flex';
|
||
tabCartazesCriar.style.color = 'var(--text-secondary)';
|
||
tabCartazesHistorico.style.color = 'var(--text-primary)';
|
||
loadCartazesHistory();
|
||
}
|
||
}
|
||
|
||
if (tabCartazesCriar) tabCartazesCriar.addEventListener('click', () => switchCartazTab('criar'));
|
||
if (tabCartazesHistorico) tabCartazesHistorico.addEventListener('click', () => switchCartazTab('historico'));
|
||
|
||
// RENDERIZAR CONTEÚDO DO CARTAZ EM TEMPO REAL
|
||
function renderCartazContentFromInputs() {
|
||
const rawCards = (cartazCardsInput?.value || '').split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
||
const subtitulo = (cartazSubtituloInput?.value || '').trim();
|
||
const rodape = (cartazRodapeInput?.value || '').trim();
|
||
|
||
let html = '';
|
||
if (subtitulo) {
|
||
html += `<div style="text-align: center; font-size: 1.15rem; font-weight: 600; opacity: 0.9; margin-bottom: 12px; letter-spacing: 0.3px;">${subtitulo}</div>`;
|
||
}
|
||
|
||
if (rawCards.length > 0) {
|
||
html += `<div style="display: flex; flex-direction: column; gap: 10px; width: 100%;">`;
|
||
rawCards.forEach((line) => {
|
||
html += `
|
||
<div style="background: rgba(255, 255, 255, 0.4); backdrop-filter: blur(4px); border: 2px solid rgba(0, 0, 0, 0.08); border-radius: 12px; padding: 12px 16px; font-size: 1.05rem; font-weight: 600; display: flex; align-items: center; gap: 10px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);">
|
||
<span>${line}</span>
|
||
</div>
|
||
`;
|
||
});
|
||
html += `</div>`;
|
||
} else {
|
||
html += `<p style="text-align: center; color: rgba(0,0,0,0.4); font-style: italic; padding: 20px 0;">Digite os destaques ou escolha um modelo ao lado.</p>`;
|
||
}
|
||
|
||
if (rodape) {
|
||
html += `<div style="margin-top: auto; padding-top: 14px; text-align: center; font-size: 0.85rem; font-weight: 600; opacity: 0.75; border-top: 1px dashed rgba(0,0,0,0.15);">${rodape}</div>`;
|
||
}
|
||
|
||
cartazPreviewContent.innerHTML = html;
|
||
if (cartazHtmlEditor) cartazHtmlEditor.value = html;
|
||
}
|
||
|
||
// Live Style Updates
|
||
function updateLivePreview() {
|
||
const bg = cartazBgColor.value;
|
||
const text = cartazTextColor.value;
|
||
const font = cartazFontSelect.value;
|
||
const border = cartazBorderSelect.value;
|
||
const title = cartazTituloInput.value.trim() || 'Título do Cartaz';
|
||
const orientacao = document.getElementById('cartazOrientacaoSelect') ? document.getElementById('cartazOrientacaoSelect').value : 'retrato';
|
||
|
||
cartazPrintableArea.style.backgroundColor = bg;
|
||
cartazPrintableArea.style.color = text;
|
||
cartazPrintableArea.style.fontFamily = font;
|
||
|
||
cartazPrintableArea.style.width = '100%';
|
||
cartazPrintableArea.style.maxWidth = orientacao === 'paisagem' ? '680px' : '480px';
|
||
cartazPrintableArea.style.aspectRatio = orientacao === 'paisagem' ? '1.414 / 1' : '1 / 1.414';
|
||
cartazPrintableArea.style.height = 'auto';
|
||
|
||
cartazPreviewTitle.textContent = title;
|
||
cartazPreviewTitle.style.borderColor = text;
|
||
|
||
if (border === 'none') {
|
||
cartazPrintableArea.style.border = 'none';
|
||
} else {
|
||
cartazPrintableArea.style.border = `${border} ${text}`;
|
||
}
|
||
}
|
||
|
||
// Event Listeners para inputs em tempo real
|
||
[
|
||
cartazTituloInput,
|
||
cartazSubtituloInput,
|
||
cartazCardsInput,
|
||
cartazRodapeInput
|
||
].forEach(elem => {
|
||
if (elem) {
|
||
elem.addEventListener('input', () => {
|
||
renderCartazContentFromInputs();
|
||
updateLivePreview();
|
||
});
|
||
}
|
||
});
|
||
|
||
[
|
||
cartazBgColor,
|
||
cartazTextColor,
|
||
cartazFontSelect,
|
||
cartazBorderSelect,
|
||
document.getElementById('cartazOrientacaoSelect')
|
||
].forEach(elem => {
|
||
if (elem) elem.addEventListener('input', updateLivePreview);
|
||
if (elem && elem.tagName === 'SELECT') elem.addEventListener('change', updateLivePreview);
|
||
});
|
||
|
||
// Preset Palettes
|
||
document.querySelectorAll('.btn-palette-preset').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
const bg = btn.dataset.bg;
|
||
const text = btn.dataset.text;
|
||
cartazBgColor.value = bg;
|
||
cartazTextColor.value = text;
|
||
updateLivePreview();
|
||
});
|
||
});
|
||
|
||
// Gerar Ilustração com IA (MiniMax T2I)
|
||
if (btnGenerateCartazImage) {
|
||
btnGenerateCartazImage.addEventListener('click', async () => {
|
||
const prompt = (cartazPromptImagem?.value || '').trim();
|
||
if (!prompt) {
|
||
await showCustomAlert('Aviso', 'Digite uma descrição para a ilustração no campo acima.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
btnGenerateCartazImage.disabled = true;
|
||
btnGenerateCartazImage.textContent = '⏳ Gerando com IA...';
|
||
|
||
const fullPrompt = `${prompt}, high resolution children book illustration, cute Pixar 3D, colorful, no text, clean vector`;
|
||
|
||
const resp = await fetch('/api/comics/generate-frame', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ prompt: fullPrompt })
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
const err = await resp.json();
|
||
throw new Error(err.error || 'Falha ao gerar ilustração');
|
||
}
|
||
|
||
const data = await resp.json();
|
||
if (data.imageUrl) {
|
||
cartazPreviewImage.src = data.imageUrl;
|
||
cartazPreviewImageContainer.style.display = 'block';
|
||
updateLivePreview();
|
||
await showCustomAlert('Ilustração Pronta!', 'A imagem foi gerada e adicionada ao cartaz com sucesso.');
|
||
}
|
||
} catch (err) {
|
||
console.error('Erro ao gerar ilustração do cartaz:', err);
|
||
await showCustomAlert('Erro', 'Não foi possível gerar a ilustração: ' + err.message);
|
||
} finally {
|
||
btnGenerateCartazImage.disabled = false;
|
||
btnGenerateCartazImage.innerHTML = '<span>🪄 Gerar Ilustração com IA</span>';
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnRemoveCartazImage) {
|
||
btnRemoveCartazImage.addEventListener('click', () => {
|
||
cartazPreviewImage.src = '';
|
||
cartazPreviewImageContainer.style.display = 'none';
|
||
if (cartazPromptImagem) cartazPromptImagem.value = '';
|
||
updateLivePreview();
|
||
});
|
||
}
|
||
|
||
// --- GERAR CONTEÚDO COM PEDAGOGIA (IA) ---
|
||
if (btnGenerateCartaz) {
|
||
btnGenerateCartaz.addEventListener('click', async () => {
|
||
const titulo = (cartazTituloInput?.value || '').trim();
|
||
const subtitulo = (cartazSubtituloInput?.value || '').trim();
|
||
const promptImagem = (cartazPromptImagem?.value || '').trim();
|
||
const orientacao = document.getElementById('cartazOrientacaoSelect')?.value || 'retrato';
|
||
|
||
if (!titulo) {
|
||
await showCustomAlert('Aviso', 'Por favor, informe ao menos o Título Principal do Cartaz.');
|
||
return;
|
||
}
|
||
|
||
btnGenerateCartaz.disabled = true;
|
||
if (cartazGenerateLoader) cartazGenerateLoader.style.display = 'flex';
|
||
|
||
try {
|
||
const res = await fetch('/api/cartazes/generate', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
titulo,
|
||
tema: subtitulo || titulo,
|
||
corFundo: cartazBgColor?.value || '#fff9eb',
|
||
corTexto: cartazTextColor?.value || '#2d3748',
|
||
layout: 'educativo',
|
||
promptImagem,
|
||
tamanho: 'A4',
|
||
orientacao
|
||
})
|
||
});
|
||
|
||
const data = await res.json();
|
||
if (res.ok) {
|
||
activeCartazData = data;
|
||
|
||
if (data.conteudo) {
|
||
// Extrair texto limpo de tópicos/cards para o input editável
|
||
const tempDiv = document.createElement('div');
|
||
tempDiv.innerHTML = data.conteudo;
|
||
const items = Array.from(tempDiv.querySelectorAll('li, p, div')).map(el => el.textContent.trim()).filter(t => t.length > 5 && !t.toLowerCase().includes(titulo.toLowerCase()));
|
||
|
||
if (items.length > 0) {
|
||
cartazCardsInput.value = items.slice(0, 6).join('\n');
|
||
} else {
|
||
cartazCardsInput.value = tempDiv.textContent.trim();
|
||
}
|
||
}
|
||
|
||
if (data.imagemUrl) {
|
||
cartazPreviewImage.src = data.imagemUrl;
|
||
cartazPreviewImageContainer.style.display = 'block';
|
||
}
|
||
|
||
renderCartazContentFromInputs();
|
||
updateLivePreview();
|
||
showToast('Cartaz diagramado pela PedagogIA com sucesso!', 'success');
|
||
} else {
|
||
await showCustomAlert('Erro', data.error || 'Erro ao gerar cartaz com IA.');
|
||
}
|
||
} catch (err) {
|
||
console.error('Erro ao gerar cartaz:', err);
|
||
await showCustomAlert('Erro', 'Não foi possível conectar ao assistente: ' + err.message);
|
||
} finally {
|
||
btnGenerateCartaz.disabled = false;
|
||
if (cartazGenerateLoader) cartazGenerateLoader.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- EXPORTAR PNG EM ALTA RESOLUÇÃO ---
|
||
if (btnExportCartazPNG) {
|
||
btnExportCartazPNG.addEventListener('click', async () => {
|
||
try {
|
||
btnExportCartazPNG.disabled = true;
|
||
btnExportCartazPNG.textContent = '⏳ Gerando PNG...';
|
||
|
||
if (typeof html2canvas === 'undefined') {
|
||
throw new Error('Biblioteca html2canvas não disponível.');
|
||
}
|
||
|
||
const canvas = await html2canvas(cartazPrintableArea, {
|
||
scale: 3, // Alta resolução 300 DPI equivalente
|
||
useCORS: true,
|
||
logging: false,
|
||
backgroundColor: cartazBgColor.value || '#fff9eb'
|
||
});
|
||
|
||
const title = (cartazTituloInput.value || 'cartaz_pedagog').replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
||
const link = document.createElement('a');
|
||
link.download = `${title}.png`;
|
||
link.href = canvas.toDataURL('image/png');
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
link.remove();
|
||
|
||
} catch (err) {
|
||
console.error('Erro ao exportar PNG:', err);
|
||
await showCustomAlert('Erro', 'Não foi possível gerar o PNG: ' + err.message);
|
||
} finally {
|
||
btnExportCartazPNG.disabled = false;
|
||
btnExportCartazPNG.textContent = '🖼️ Baixar PNG';
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- EXPORTAR PDF IMPRIMÍVEL ---
|
||
if (btnExportCartazPDF) {
|
||
btnExportCartazPDF.addEventListener('click', async () => {
|
||
try {
|
||
btnExportCartazPDF.disabled = true;
|
||
btnExportCartazPDF.textContent = '⏳ Criando PDF...';
|
||
|
||
if (typeof html2canvas === 'undefined' || typeof window.jspdf === 'undefined') {
|
||
throw new Error('Bibliotecas de PDF necessárias não disponíveis.');
|
||
}
|
||
|
||
const orientacao = document.getElementById('cartazOrientacaoSelect')?.value === 'paisagem' ? 'l' : 'p';
|
||
const canvas = await html2canvas(cartazPrintableArea, {
|
||
scale: 3,
|
||
useCORS: true,
|
||
logging: false,
|
||
backgroundColor: cartazBgColor.value || '#fff9eb'
|
||
});
|
||
|
||
const imgData = canvas.toDataURL('image/jpeg', 0.95);
|
||
const { jsPDF } = window.jspdf;
|
||
const pdf = new jsPDF(orientacao, 'pt', 'a4');
|
||
const pdfWidth = orientacao === 'l' ? 842 : 595;
|
||
const pdfHeight = orientacao === 'l' ? 595 : 842;
|
||
|
||
pdf.addImage(imgData, 'JPEG', 0, 0, pdfWidth, pdfHeight);
|
||
const title = (cartazTituloInput.value || 'cartaz_pedagog').replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
|
||
pdf.save(`${title}.pdf`);
|
||
|
||
} catch (err) {
|
||
console.error('Erro ao exportar PDF:', err);
|
||
await showCustomAlert('Erro', 'Não foi possível gerar o PDF: ' + err.message);
|
||
} finally {
|
||
btnExportCartazPDF.disabled = false;
|
||
btnExportCartazPDF.textContent = '📄 Baixar PDF';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Print Poster
|
||
if (btnPrintCartaz) {
|
||
btnPrintCartaz.addEventListener('click', () => {
|
||
const title = cartazTituloInput.value.trim() || 'Cartaz Pedagógico';
|
||
const bg = cartazBgColor.value;
|
||
const text = cartazTextColor.value;
|
||
const font = cartazFontSelect.value;
|
||
const border = cartazBorderSelect.value;
|
||
const tamanho = document.getElementById('cartazTamanhoSelect') ? document.getElementById('cartazTamanhoSelect').value : 'A4';
|
||
const orientacao = document.getElementById('cartazOrientacaoSelect') ? document.getElementById('cartazOrientacaoSelect').value : 'retrato';
|
||
|
||
const imgContainerHtml = cartazPreviewImageContainer.style.display === 'block' && cartazPreviewImageContainer.style.position !== 'absolute'
|
||
? `<div style="width: 100%; margin-bottom: 20px; border-radius: 8px; overflow: hidden; border: 2px solid rgba(0,0,0,0.1); text-align: center; background: rgba(0,0,0,0.02);"><img src="${cartazPreviewImage.src}" style="max-width: 100%; max-height: 350px; object-fit: contain; display: inline-block;"></div>`
|
||
: '';
|
||
|
||
let printContent = '';
|
||
const hasCanvasItems = cartazPrintableArea.querySelectorAll('.canvas-item').length > 0;
|
||
|
||
if (hasCanvasItems) {
|
||
// Clone printable area and cleanup handles / active state borders for print output
|
||
const tempContainer = document.createElement('div');
|
||
tempContainer.innerHTML = cartazPrintableArea.innerHTML;
|
||
tempContainer.querySelectorAll('.canvas-resize-handle').forEach(h => h.remove());
|
||
tempContainer.querySelectorAll('.canvas-item').forEach(item => {
|
||
item.classList.remove('active-item');
|
||
item.removeAttribute('contenteditable');
|
||
if (item.style.border && item.style.border.includes('dashed')) {
|
||
item.style.border = 'none';
|
||
}
|
||
});
|
||
printContent = tempContainer.innerHTML;
|
||
} else {
|
||
printContent = `
|
||
<h1>${title}</h1>
|
||
${imgContainerHtml}
|
||
<div class="content-area">
|
||
${cartazPreviewContent.innerHTML}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
const printWindow = window.open('', '_blank');
|
||
printWindow.document.write(`
|
||
<html>
|
||
<head>
|
||
<title>${title} - PedaGog</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&family=Caveat:wght@400;700&family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
* {
|
||
-webkit-print-color-adjust: exact !important;
|
||
print-color-adjust: exact !important;
|
||
box-sizing: border-box;
|
||
}
|
||
@page {
|
||
size: ${tamanho} ${orientacao === 'paisagem' ? 'landscape' : 'portrait'};
|
||
margin: 10mm;
|
||
}
|
||
body {
|
||
font-family: ${font};
|
||
margin: 0;
|
||
padding: 20px;
|
||
background-color: #f7fafc;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
min-height: 100vh;
|
||
}
|
||
.cartaz-frame {
|
||
background-color: ${bg};
|
||
color: ${text};
|
||
padding: 40px;
|
||
border-radius: 16px;
|
||
width: 100%;
|
||
max-width: ${orientacao === 'paisagem' ? '1200px' : '800px'};
|
||
aspect-ratio: ${orientacao === 'paisagem' ? '1.414 / 1' : '1 / 1.414'};
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||
box-sizing: border-box;
|
||
border: ${border === 'none' ? 'none' : `${border} ${text}`};
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
overflow: hidden;
|
||
container-type: inline-size;
|
||
}
|
||
h1 {
|
||
text-align: center;
|
||
margin-top: 0;
|
||
font-size: 3.5rem;
|
||
border-bottom: 3px solid ${text};
|
||
padding-bottom: 16px;
|
||
margin-bottom: 30px;
|
||
}
|
||
.content-area {
|
||
font-size: 1.5rem;
|
||
line-height: 1.6;
|
||
display: flex;
|
||
flex-direction: ${orientacao === 'paisagem' ? 'row' : 'column'};
|
||
flex-wrap: wrap;
|
||
gap: 20px;
|
||
justify-content: center;
|
||
}
|
||
/* Suporte para cards na content-area */
|
||
.content-area > div, .content-area > p, .content-area > ul {
|
||
box-sizing: border-box;
|
||
background: rgba(255, 255, 255, 0.3);
|
||
padding: 20px;
|
||
border-radius: 12px;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||
width: ${orientacao === 'paisagem' ? 'calc(50% - 20px)' : '100%'};
|
||
flex: ${orientacao === 'paisagem' ? '1 1 calc(50% - 20px)' : '0 0 auto'};
|
||
margin: 0;
|
||
}
|
||
ul {
|
||
margin-left: 20px;
|
||
}
|
||
li {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
/* Estilos específicos do Canvas no PDF/Print */
|
||
.canvas-item {
|
||
position: absolute !important;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
@media print {
|
||
body { background: transparent; padding: 0; display: block; }
|
||
.cartaz-frame {
|
||
box-shadow: none;
|
||
border-radius: 0;
|
||
max-width: 100%;
|
||
width: 100%;
|
||
height: auto;
|
||
min-height: 100%;
|
||
padding: 30px;
|
||
}
|
||
button { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="cartaz-frame">
|
||
${printContent}
|
||
</div>
|
||
<script>
|
||
window.onload = function() {
|
||
window.print();
|
||
}
|
||
<\/script>
|
||
</body>
|
||
</html>
|
||
`);
|
||
printWindow.document.close();
|
||
});
|
||
}
|
||
|
||
// Load History list
|
||
async function loadCartazesHistory() {
|
||
if (!cartazesHistoryContainer) return;
|
||
cartazesHistoryContainer.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">Carregando histórico...</div>';
|
||
|
||
try {
|
||
const res = await fetch('/api/cartazes/list', {
|
||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||
});
|
||
const data = await res.json();
|
||
cartazesHistoryContainer.innerHTML = '';
|
||
|
||
if (!data || data.length === 0) {
|
||
cartazesNoData.style.display = 'block';
|
||
return;
|
||
}
|
||
cartazesNoData.style.display = 'none';
|
||
|
||
data.forEach(item => {
|
||
const card = document.createElement('div');
|
||
card.style.background = 'var(--bg-tertiary)';
|
||
card.style.border = '1px solid var(--border-light)';
|
||
card.style.borderRadius = '10px';
|
||
card.style.padding = '16px';
|
||
card.style.display = 'flex';
|
||
card.style.flexDirection = 'column';
|
||
card.style.justifyContent = 'space-between';
|
||
card.style.gap = '12px';
|
||
card.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
|
||
|
||
const thumbnail = item.imagemUrl
|
||
? `<div style="height: 120px; overflow:hidden; border-radius: 6px; border: 1px solid var(--border-light); margin-bottom: 6px;">
|
||
<img src="${item.imagemUrl}" style="width:100%; height:100%; object-fit:cover;">
|
||
</div>`
|
||
: `<div style="height: 120px; border-radius: 6px; background: ${item.corFundo || '#fff9eb'}; color: ${item.corTexto || '#2d3748'}; display:flex; align-items:center; justify-content:center; border: 1px solid var(--border-light); margin-bottom: 6px; font-weight: bold; font-size: 0.8rem; overflow: hidden; padding: 10px; text-align:center;">
|
||
${item.titulo}
|
||
</div>`;
|
||
|
||
card.innerHTML = `
|
||
<div>
|
||
${thumbnail}
|
||
<h5 style="margin: 0; font-size: 1rem; color: var(--text-primary); font-family: 'Outfit', sans-serif;">${item.titulo}</h5>
|
||
<p style="margin: 4px 0 0 0; font-size: 0.8rem; color: var(--text-secondary);">Foco: ${item.tema}</p>
|
||
<span style="font-size: 0.75rem; color: var(--text-muted); display: block; margin-top: 6px;">Layout: ${item.layout.toUpperCase()}</span>
|
||
</div>
|
||
<div style="display: flex; gap: 8px; justify-content: flex-end; border-top: 1px solid var(--border-light); padding-top: 10px;">
|
||
<button class="btn-ver-cartaz btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.8rem; border-radius: 6px; cursor: pointer; background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.3); color: #60a5fa;">Ver / Editar</button>
|
||
<button class="btn-del-cartaz btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.8rem; border-radius: 6px; cursor: pointer; color: #ef4444; border-color: rgba(239, 68, 68, 0.3);">🗑️</button>
|
||
</div>
|
||
`;
|
||
cartazesHistoryContainer.appendChild(card);
|
||
});
|
||
|
||
// Bind Ver/Editar Buttons
|
||
document.querySelectorAll('.btn-ver-cartaz').forEach(btn => {
|
||
btn.addEventListener('click', async (e) => {
|
||
const id = e.currentTarget.dataset.id;
|
||
try {
|
||
const r = await fetch(`/api/cartazes/${id}`, {
|
||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||
});
|
||
const poster = await r.json();
|
||
|
||
// Set Inputs
|
||
cartazTituloInput.value = poster.titulo;
|
||
cartazTemaInput.value = poster.tema;
|
||
cartazLayoutSelect.value = poster.layout;
|
||
cartazBgColor.value = poster.corFundo;
|
||
cartazTextColor.value = poster.corTexto;
|
||
cartazHtmlEditor.value = poster.conteudo;
|
||
cartazPreviewContent.innerHTML = poster.conteudo;
|
||
|
||
if (poster.imagemUrl) {
|
||
cartazPreviewImage.src = poster.imagemUrl;
|
||
cartazPreviewImageContainer.style.display = 'block';
|
||
} else {
|
||
cartazPreviewImageContainer.style.display = 'none';
|
||
}
|
||
|
||
updateLivePreview();
|
||
switchCartazTab('criar');
|
||
} catch (err) {
|
||
showToast('Erro ao carregar cartaz.', 'error');
|
||
}
|
||
});
|
||
});
|
||
|
||
// Bind Delete Buttons
|
||
document.querySelectorAll('.btn-del-cartaz').forEach(btn => {
|
||
btn.addEventListener('click', async (e) => {
|
||
if (!confirm('Tem certeza que deseja apagar este cartaz?')) return;
|
||
const id = e.currentTarget.dataset.id;
|
||
try {
|
||
const r = await fetch(`/api/cartazes/${id}`, {
|
||
method: 'DELETE',
|
||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||
});
|
||
if (r.ok) {
|
||
showToast('Cartaz deletado com sucesso.', 'success');
|
||
loadCartazesHistory();
|
||
} else {
|
||
showToast('Erro ao deletar cartaz.', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Erro de conexão ao deletar cartaz.', 'error');
|
||
}
|
||
});
|
||
});
|
||
|
||
} catch (err) {
|
||
console.error(err);
|
||
cartazesHistoryContainer.innerHTML = '<div style="color: #ef4444; padding: 20px;">Erro ao carregar histórico de cartazes.</div>';
|
||
}
|
||
}
|
||
|
||
// --- MODO DESIGN CANVAS (EDITOR INTERATIVO DE CARTAZES) ---
|
||
const btnCanvasModeToggle = document.getElementById('btnCanvasModeToggle');
|
||
const canvasControlPanel = document.getElementById('canvasControlPanel');
|
||
const btnCanvasAddText = document.getElementById('btnCanvasAddText');
|
||
const btnCanvasAddCard = document.getElementById('btnCanvasAddCard');
|
||
const canvasFontFamily = document.getElementById('canvasFontFamily');
|
||
const canvasFontSize = document.getElementById('canvasFontSize');
|
||
const canvasTextColor = document.getElementById('canvasTextColor');
|
||
const canvasBgColorInput = document.getElementById('canvasBgColor');
|
||
const canvasRotation = document.getElementById('canvasRotation');
|
||
const canvasOpacity = document.getElementById('canvasOpacity');
|
||
const btnCanvasZIndexUp = document.getElementById('btnCanvasZIndexUp');
|
||
const btnCanvasZIndexDown = document.getElementById('btnCanvasZIndexDown');
|
||
const btnCanvasDelete = document.getElementById('btnCanvasDelete');
|
||
|
||
// Novos controles do Painel de Assets Lateral
|
||
const canvasAssetSearch = document.getElementById('canvasAssetSearch');
|
||
const canvasAssetsContainer = document.getElementById('canvasAssetsContainer');
|
||
const btnTabAssetPostits = document.getElementById('btnTabAssetPostits');
|
||
const btnTabAssetStickers = document.getElementById('btnTabAssetStickers');
|
||
const btnTabAssetFormas = document.getElementById('btnTabAssetFormas');
|
||
|
||
let canvasModeActive = false;
|
||
let selectedCanvasItem = null;
|
||
let isDraggingCanvas = false;
|
||
let isResizingCanvas = false;
|
||
let dragStartX, dragStartY;
|
||
let dragStartLeft, dragStartTop;
|
||
let dragStartWidth, dragStartHeight;
|
||
let activeAssetTab = 'postit';
|
||
|
||
// --- ESTÚDIO DE CENAS EM CAMADAS (novo layer manager) ---
|
||
const btnDecomposeScene = document.getElementById('btnDecomposeScene');
|
||
const btnAddSceneLayer = document.getElementById('btnAddSceneLayer');
|
||
const sceneLayerManagerList = document.getElementById('sceneLayerManagerList');
|
||
const sceneDecomposeLoader = document.getElementById('sceneDecomposeLoader');
|
||
const sceneDecomposeStatus = document.getElementById('sceneDecomposeStatus');
|
||
|
||
// Estado das camadas
|
||
let sceneLayers = [];
|
||
let sceneLayerCounter = 0;
|
||
|
||
// Cria uma linha de camada no painel
|
||
function createSceneLayerRow(id, isBackground) {
|
||
const row = document.createElement('div');
|
||
row.dataset.layerId = id;
|
||
row.style.cssText = `display:flex; align-items:center; gap:6px; padding:6px 8px;
|
||
background:rgba(255,255,255,0.04); border:1px solid rgba(139,92,246,0.2);
|
||
border-radius:8px; transition: border-color 0.2s;`;
|
||
|
||
const badge = document.createElement('div');
|
||
badge.style.cssText = `min-width:22px; height:22px; border-radius:50%;
|
||
background:${isBackground ? 'rgba(16,163,127,0.3)' : 'rgba(139,92,246,0.3)'};
|
||
display:flex; align-items:center; justify-content:center; font-size:0.7rem; font-weight:700;
|
||
color:${isBackground ? '#10a37f' : '#a78bfa'}; flex-shrink:0;`;
|
||
badge.textContent = isBackground ? '🌄' : sceneLayers.filter(l => !l.isBackground).length;
|
||
|
||
const input = document.createElement('input');
|
||
input.type = 'text';
|
||
input.placeholder = isBackground
|
||
? 'Cenário de fundo (Ex: parque de diversões)'
|
||
: 'Elemento (Ex: menino de 6 anos)';
|
||
input.style.cssText = `flex:1; background:transparent; border:none; outline:none;
|
||
color:var(--text-primary); font-size:0.82rem; padding:2px 0; min-width:0;`;
|
||
input.addEventListener('input', () => {
|
||
const layer = sceneLayers.find(l => l.id === id);
|
||
if (layer) layer.description = input.value;
|
||
});
|
||
|
||
// Botões de ação
|
||
const btnVisibility = document.createElement('button');
|
||
btnVisibility.title = 'Ocultar/Mostrar camada no canvas';
|
||
btnVisibility.innerHTML = '👁️';
|
||
btnVisibility.style.cssText = `background:none; border:none; cursor:pointer; font-size:0.9rem;
|
||
padding:2px; opacity:1; transition:opacity 0.2s; flex-shrink:0;`;
|
||
let visible = true;
|
||
btnVisibility.addEventListener('click', () => {
|
||
const layer = sceneLayers.find(l => l.id === id);
|
||
if (!layer || !layer.canvasItem) return;
|
||
visible = !visible;
|
||
layer.visible = visible;
|
||
layer.canvasItem.style.display = visible ? '' : 'none';
|
||
btnVisibility.style.opacity = visible ? '1' : '0.3';
|
||
btnVisibility.title = visible ? 'Ocultar camada' : 'Mostrar camada';
|
||
});
|
||
|
||
const btnPin = document.createElement('button');
|
||
btnPin.title = 'Fixar posição (impede arrastar)';
|
||
btnPin.innerHTML = '📌';
|
||
btnPin.style.cssText = `background:none; border:none; cursor:pointer; font-size:0.9rem;
|
||
padding:2px; opacity:0.4; transition:opacity 0.2s; flex-shrink:0;`;
|
||
let pinned = false;
|
||
btnPin.addEventListener('click', () => {
|
||
const layer = sceneLayers.find(l => l.id === id);
|
||
if (!layer || !layer.canvasItem) return;
|
||
pinned = !pinned;
|
||
layer.pinned = pinned;
|
||
layer.canvasItem.style.pointerEvents = pinned ? 'none' : '';
|
||
layer.canvasItem.style.cursor = pinned ? 'default' : 'move';
|
||
btnPin.style.opacity = pinned ? '1' : '0.4';
|
||
btnPin.title = pinned ? 'Desafixar posição' : 'Fixar posição';
|
||
});
|
||
|
||
const btnDelete = document.createElement('button');
|
||
btnDelete.title = 'Remover camada';
|
||
btnDelete.innerHTML = '🗑️';
|
||
btnDelete.style.cssText = `background:none; border:none; cursor:pointer; font-size:0.9rem;
|
||
padding:2px; opacity:0.6; transition:opacity 0.2s; flex-shrink:0;`;
|
||
btnDelete.addEventListener('mouseenter', () => btnDelete.style.opacity = '1');
|
||
btnDelete.addEventListener('mouseleave', () => btnDelete.style.opacity = '0.6');
|
||
btnDelete.addEventListener('click', () => {
|
||
// Remove do canvas se já gerado
|
||
const layer = sceneLayers.find(l => l.id === id);
|
||
if (layer?.canvasItem) layer.canvasItem.remove();
|
||
// Remove do state
|
||
sceneLayers = sceneLayers.filter(l => l.id !== id);
|
||
// Remove do DOM
|
||
row.remove();
|
||
// Reindexar badges dos elementos
|
||
reindexLayerBadges();
|
||
});
|
||
|
||
row.appendChild(badge);
|
||
row.appendChild(input);
|
||
row.appendChild(btnVisibility);
|
||
row.appendChild(btnPin);
|
||
if (!isBackground) row.appendChild(btnDelete); // Fundo não pode ser deletado da lista
|
||
|
||
return { row, input, badge };
|
||
}
|
||
|
||
function reindexLayerBadges() {
|
||
let elIdx = 0;
|
||
sceneLayers.forEach(l => {
|
||
if (!l.isBackground && l.rowBadge) {
|
||
l.rowBadge.textContent = ++elIdx;
|
||
}
|
||
});
|
||
}
|
||
|
||
function addSceneLayerEntry(isBackground = false) {
|
||
const id = `sl_${++sceneLayerCounter}`;
|
||
const layerObj = { id, description: '', isBackground, visible: true, pinned: false, canvasItem: null };
|
||
sceneLayers.push(layerObj);
|
||
|
||
const { row, input, badge } = createSceneLayerRow(id, isBackground);
|
||
layerObj.rowInput = input;
|
||
layerObj.rowBadge = badge;
|
||
if (sceneLayerManagerList) sceneLayerManagerList.appendChild(row);
|
||
input.focus();
|
||
return layerObj;
|
||
}
|
||
|
||
// Inicializa com camada de fundo padrão
|
||
addSceneLayerEntry(true);
|
||
|
||
if (btnAddSceneLayer) {
|
||
btnAddSceneLayer.addEventListener('click', () => {
|
||
addSceneLayerEntry(false);
|
||
reindexLayerBadges();
|
||
});
|
||
}
|
||
|
||
// ===== REMOÇÃO DE FUNDO BRANCO VIA CANVAS API =====
|
||
async function removeWhiteBackground(imgUrl, threshold = 235) {
|
||
return new Promise((resolve) => {
|
||
const img = new Image();
|
||
img.crossOrigin = 'anonymous';
|
||
img.onload = () => {
|
||
const offscreen = document.createElement('canvas');
|
||
offscreen.width = img.naturalWidth;
|
||
offscreen.height = img.naturalHeight;
|
||
const ctx = offscreen.getContext('2d', { willReadFrequently: true });
|
||
ctx.drawImage(img, 0, 0);
|
||
|
||
const imageData = ctx.getImageData(0, 0, offscreen.width, offscreen.height);
|
||
const data = imageData.data;
|
||
const softRange = 20;
|
||
|
||
for (let i = 0; i < data.length; i += 4) {
|
||
const r = data[i], g = data[i + 1], b = data[i + 2];
|
||
const minChannel = Math.min(r, g, b);
|
||
|
||
if (minChannel >= threshold) {
|
||
data[i + 3] = 0;
|
||
} else if (minChannel >= threshold - softRange) {
|
||
const ratio = (minChannel - (threshold - softRange)) / softRange;
|
||
data[i + 3] = Math.round((1 - ratio) * data[i + 3]);
|
||
}
|
||
}
|
||
|
||
ctx.putImageData(imageData, 0, 0);
|
||
resolve(offscreen.toDataURL('image/png'));
|
||
};
|
||
img.onerror = () => resolve(imgUrl);
|
||
img.src = imgUrl;
|
||
});
|
||
}
|
||
|
||
// ===== ADICIONA ITEM DE CAMADA NO CANVAS =====
|
||
function addSceneLayerToCanvas(layerData) {
|
||
if (!cartazPrintableArea) return null;
|
||
|
||
const item = document.createElement('div');
|
||
item.className = 'canvas-item canvas-scene-layer';
|
||
item.setAttribute('data-layer-name', layerData.name || '');
|
||
item.setAttribute('data-rotation', '0');
|
||
item.style.cssText = `position:absolute; background:transparent; box-sizing:border-box;`;
|
||
item.style.zIndex = layerData.isBackground ? '0' : String(10 + (layerData.zOffset || 0));
|
||
|
||
// Cria a imagem SEM usar innerHTML (preserva o handle de resize)
|
||
const imgEl = document.createElement('img');
|
||
imgEl.style.cssText = `width:100%; height:100%; object-fit:${layerData.isBackground ? 'cover' : 'contain'}; pointer-events:none; display:block;`;
|
||
imgEl.src = layerData.url;
|
||
item.appendChild(imgEl);
|
||
|
||
// Handle de resize — criado uma única vez e permanece
|
||
const handle = document.createElement('div');
|
||
handle.className = 'canvas-resize-handle';
|
||
item.appendChild(handle);
|
||
|
||
if (layerData.isBackground) {
|
||
item.style.left = '0';
|
||
item.style.top = '0';
|
||
item.style.width = '100%';
|
||
item.style.height = '100%';
|
||
item.title = '🖼️ Cenário de Fundo';
|
||
} else {
|
||
item.style.left = (layerData.left || 10) + '%';
|
||
item.style.top = (layerData.top || 10) + '%';
|
||
item.style.width = (layerData.width || 30) + '%';
|
||
item.style.height = (layerData.height || 30) + '%';
|
||
item.title = '🎬 ' + (layerData.name || 'Elemento');
|
||
|
||
// Remove fundo branco: atualiza apenas o src da img (preserva o handle)
|
||
removeWhiteBackground(layerData.url).then(transparentUrl => {
|
||
imgEl.src = transparentUrl;
|
||
});
|
||
}
|
||
|
||
cartazPrintableArea.appendChild(item);
|
||
setupCanvasItemEvents(item);
|
||
return item;
|
||
}
|
||
|
||
// ===== BOTÃO CRIAR CENA =====
|
||
if (btnDecomposeScene) {
|
||
btnDecomposeScene.addEventListener('click', async () => {
|
||
// Validar que há pelo menos fundo + 1 elemento com descrição
|
||
const bgLayer = sceneLayers.find(l => l.isBackground);
|
||
if (!bgLayer?.description?.trim()) {
|
||
showToast('Descreva o cenário de fundo (Camada 🌄)!', 'warning');
|
||
if (bgLayer?.rowInput) bgLayer.rowInput.focus();
|
||
return;
|
||
}
|
||
const elementLayers = sceneLayers.filter(l => !l.isBackground && l.description?.trim());
|
||
if (elementLayers.length === 0) {
|
||
showToast('Adicione pelo menos um elemento (➕ Adicionar Camada)!', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (!canvasModeActive) {
|
||
if (cartazEditArea && cartazEditArea.style.display !== 'none') {
|
||
btnEditCartazToggle && btnEditCartazToggle.click();
|
||
}
|
||
enterCanvasMode();
|
||
}
|
||
|
||
btnDecomposeScene.disabled = true;
|
||
btnDecomposeScene.style.opacity = '0.6';
|
||
if (sceneDecomposeLoader) sceneDecomposeLoader.style.display = 'flex';
|
||
|
||
// Remover camadas anteriores do canvas
|
||
cartazPrintableArea.querySelectorAll('.canvas-scene-layer').forEach(el => el.remove());
|
||
sceneLayers.forEach(l => { l.canvasItem = null; });
|
||
|
||
// Gerar cada camada individualmente (uma por uma para acompanhar progresso)
|
||
const totalLayers = 1 + elementLayers.length; // fundo + elementos
|
||
let done = 0;
|
||
|
||
const updateStatus = () => {
|
||
done++;
|
||
if (sceneDecomposeStatus) sceneDecomposeStatus.textContent = `Gerando camada ${done}/${totalLayers}...`;
|
||
};
|
||
|
||
try {
|
||
const titulo = activeCartazData?.titulo || '';
|
||
const token = localStorage.getItem('token');
|
||
|
||
// Gera todas as imagens em paralelo via API (usando os prompts diretos das camadas)
|
||
const allPrompts = [
|
||
{ isBackground: true, description: bgLayer.description, layerRef: bgLayer },
|
||
...elementLayers.map(l => ({ isBackground: false, description: l.description, layerRef: l }))
|
||
];
|
||
|
||
if (sceneDecomposeStatus) sceneDecomposeStatus.textContent = `Enviando para a IA...`;
|
||
|
||
const response = await fetch('/api/cartazes/decompose-scene', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
||
body: JSON.stringify({
|
||
titulo,
|
||
// Mandamos a estrutura de camadas diretamente (novo campo layers_manual)
|
||
layers_manual: allPrompts.map(p => ({
|
||
isBackground: p.isBackground,
|
||
description: p.description
|
||
}))
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errData = await response.json().catch(() => ({}));
|
||
throw new Error(errData.error || 'Erro na geração da cena.');
|
||
}
|
||
|
||
const data = await response.json();
|
||
if (!data.success) throw new Error('Resposta inválida do servidor.');
|
||
|
||
if (sceneDecomposeStatus) sceneDecomposeStatus.textContent = 'Montando as camadas no canvas...';
|
||
|
||
// Fundo
|
||
if (data.backgroundUrl && bgLayer) {
|
||
bgLayer.canvasItem = addSceneLayerToCanvas({
|
||
url: data.backgroundUrl, name: bgLayer.description, isBackground: true
|
||
});
|
||
}
|
||
|
||
// Elementos
|
||
const spread = [
|
||
{ left: 8, top: 32 }, { left: 42, top: 38 },
|
||
{ left: 64, top: 28 }, { left: 22, top: 56 }
|
||
];
|
||
if (Array.isArray(data.layers)) {
|
||
data.layers.forEach((layer, idx) => {
|
||
if (!layer.url) return;
|
||
const elLayerRef = elementLayers[idx];
|
||
const pos = spread[idx % spread.length];
|
||
const canvasItem = addSceneLayerToCanvas({
|
||
url: layer.url,
|
||
name: layer.name || elLayerRef?.description || `Elemento ${idx + 1}`,
|
||
isBackground: false,
|
||
left: pos.left, top: pos.top, width: 30, height: 30,
|
||
zOffset: idx + 1
|
||
});
|
||
if (elLayerRef) elLayerRef.canvasItem = canvasItem;
|
||
});
|
||
}
|
||
|
||
showToast('🎬 Cena montada! Use os botões 👁️ 📌 🗑️ para controlar cada camada.', 'success');
|
||
|
||
} catch (err) {
|
||
console.error('[Cenas em Camadas]', err);
|
||
showToast('Erro ao criar a cena: ' + err.message, 'error');
|
||
} finally {
|
||
btnDecomposeScene.disabled = false;
|
||
btnDecomposeScene.style.opacity = '1';
|
||
if (sceneDecomposeLoader) sceneDecomposeLoader.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Cria um item de camada de imagem direto no canvas (background ou sticker)
|
||
// Remoção de fundo branco via Canvas API — gera PNG com transparência real
|
||
async function removeWhiteBackground(imgUrl, threshold = 235) {
|
||
return new Promise((resolve) => {
|
||
const img = new Image();
|
||
img.crossOrigin = 'anonymous'; // necessário para getImageData funcionar
|
||
img.onload = () => {
|
||
const offscreen = document.createElement('canvas');
|
||
offscreen.width = img.naturalWidth;
|
||
offscreen.height = img.naturalHeight;
|
||
const ctx = offscreen.getContext('2d', { willReadFrequently: true });
|
||
ctx.drawImage(img, 0, 0);
|
||
|
||
const imageData = ctx.getImageData(0, 0, offscreen.width, offscreen.height);
|
||
const data = imageData.data;
|
||
// threshold: pixels com R,G,B >= threshold são considerados "fundos a remover"
|
||
// softRange: faixa de suavização para bordas anti-aliased
|
||
const softRange = 20;
|
||
|
||
for (let i = 0; i < data.length; i += 4) {
|
||
const r = data[i];
|
||
const g = data[i + 1];
|
||
const b = data[i + 2];
|
||
|
||
// Pega o mínimo dos canais (garante que só pixels "esbranquiçados" sejam afetados)
|
||
const minChannel = Math.min(r, g, b);
|
||
|
||
if (minChannel >= threshold) {
|
||
// Área central do branco: totalmente transparente
|
||
data[i + 3] = 0;
|
||
} else if (minChannel >= threshold - softRange) {
|
||
// Zona de borda: transição linear suave para preservar contornos
|
||
const ratio = (minChannel - (threshold - softRange)) / softRange;
|
||
data[i + 3] = Math.round((1 - ratio) * data[i + 3]);
|
||
}
|
||
// Pixels abaixo do threshold - softRange: mantém opacidade original
|
||
}
|
||
|
||
ctx.putImageData(imageData, 0, 0);
|
||
resolve(offscreen.toDataURL('image/png'));
|
||
};
|
||
img.onerror = () => {
|
||
console.warn('[removeWhiteBackground] Falha ao carregar imagem, usando original:', imgUrl);
|
||
resolve(imgUrl);
|
||
};
|
||
img.src = imgUrl;
|
||
});
|
||
}
|
||
|
||
function addSceneLayerToCanvas(layerData) {
|
||
if (!canvasModeActive || !cartazPrintableArea) return;
|
||
|
||
const item = document.createElement('div');
|
||
item.className = 'canvas-item canvas-scene-layer';
|
||
item.setAttribute('data-layer-name', layerData.name || '');
|
||
item.setAttribute('data-rotation', '0');
|
||
item.style.position = 'absolute';
|
||
item.style.left = (layerData.left || 10) + '%';
|
||
item.style.top = (layerData.top || 10) + '%';
|
||
item.style.width = (layerData.width || 35) + '%';
|
||
item.style.height = (layerData.height || 35) + '%';
|
||
item.style.zIndex = layerData.isBackground ? '0' : String(10 + (layerData.zOffset || 0));
|
||
item.style.background = 'transparent';
|
||
|
||
if (layerData.isBackground) {
|
||
item.style.left = '0';
|
||
item.style.top = '0';
|
||
item.style.width = '100%';
|
||
item.style.height = '100%';
|
||
item.style.zIndex = '0';
|
||
item.style.pointerEvents = 'none';
|
||
item.innerHTML = `<img src="${layerData.url}"
|
||
style="width:100%; height:100%; object-fit:cover; pointer-events:none; display:block;">`;
|
||
item.title = '🖼️ Cenário de Fundo';
|
||
|
||
const handle = document.createElement('div');
|
||
handle.className = 'canvas-resize-handle';
|
||
item.appendChild(handle);
|
||
cartazPrintableArea.appendChild(item);
|
||
item.style.pointerEvents = 'auto';
|
||
setupCanvasItemEvents(item);
|
||
|
||
} else {
|
||
// Adiciona o item ao canvas com um placeholder de loading
|
||
item.innerHTML = `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;opacity:0.4;">
|
||
<span style="font-size:1.5rem;animation:spin 1s linear infinite;">⏳</span>
|
||
</div>`;
|
||
item.title = '🎬 ' + (layerData.name || 'Elemento');
|
||
|
||
const handle = document.createElement('div');
|
||
handle.className = 'canvas-resize-handle';
|
||
item.appendChild(handle);
|
||
cartazPrintableArea.appendChild(item);
|
||
setupCanvasItemEvents(item);
|
||
|
||
// Remove o fundo branco de forma assíncrona via Canvas API
|
||
removeWhiteBackground(layerData.url).then(transparentDataUrl => {
|
||
item.innerHTML = `<img src="${transparentDataUrl}"
|
||
style="width:100%; height:100%; object-fit:contain; pointer-events:none; display:block;">`;
|
||
const newHandle = document.createElement('div');
|
||
newHandle.className = 'canvas-resize-handle';
|
||
item.appendChild(newHandle);
|
||
});
|
||
}
|
||
|
||
return item;
|
||
}
|
||
|
||
// Renderiza a lista de camadas no painel lateral
|
||
function renderSceneLayers(layers) {
|
||
if (!sceneLayersItems) return;
|
||
sceneLayersItems.innerHTML = '';
|
||
|
||
const allLayers = [{ name: '🖼️ Cenário de Fundo', isBackground: true }, ...layers.map(l => ({ ...l }))];
|
||
allLayers.forEach((layer, index) => {
|
||
const chip = document.createElement('div');
|
||
chip.style.cssText = `display:flex; align-items:center; gap:8px; padding:5px 8px;
|
||
background:rgba(255,255,255,0.05); border:1px solid rgba(139,92,246,0.2);
|
||
border-radius:6px; font-size:0.78rem; color:var(--text-primary); cursor:pointer;`;
|
||
chip.innerHTML = `
|
||
<span style="font-size:1rem;">${layer.isBackground ? '🌄' : '✨'}</span>
|
||
<span style="flex:1;">${layer.name}</span>
|
||
<span style="font-size:0.68rem; color:var(--text-secondary);">Camada ${index}</span>
|
||
`;
|
||
chip.title = `${layer.isBackground ? 'Fundo da cena' : 'Elemento: ' + layer.name}`;
|
||
sceneLayersItems.appendChild(chip);
|
||
});
|
||
}
|
||
|
||
if (btnDecomposeScene) {
|
||
btnDecomposeScene.addEventListener('click', async () => {
|
||
const prompt = sceneDecomposePrompt ? sceneDecomposePrompt.value.trim() : '';
|
||
if (!prompt) {
|
||
showToast('Descreva a cena antes de gerar!', 'warning');
|
||
return;
|
||
}
|
||
|
||
// Garantir que o modo canvas está ativo
|
||
if (!canvasModeActive) {
|
||
if (cartazEditArea && cartazEditArea.style.display !== 'none') {
|
||
btnEditCartazToggle && btnEditCartazToggle.click();
|
||
}
|
||
enterCanvasMode();
|
||
}
|
||
|
||
// Mostrar loading
|
||
btnDecomposeScene.disabled = true;
|
||
btnDecomposeScene.style.opacity = '0.6';
|
||
if (sceneDecomposeLoader) {
|
||
sceneDecomposeLoader.style.display = 'flex';
|
||
}
|
||
if (sceneLayersList) sceneLayersList.style.display = 'none';
|
||
|
||
const statusSteps = [
|
||
'Planejando a cena com IA...',
|
||
'Identificando elementos e cenário...',
|
||
'Gerando imagem de fundo...',
|
||
'Gerando elementos individuais...',
|
||
'Montando as camadas no canvas...'
|
||
];
|
||
let stepIdx = 0;
|
||
const statusInterval = setInterval(() => {
|
||
if (sceneDecomposeStatus && stepIdx < statusSteps.length) {
|
||
sceneDecomposeStatus.textContent = statusSteps[stepIdx++];
|
||
}
|
||
}, 3500);
|
||
|
||
try {
|
||
const titulo = activeCartazData?.titulo || '';
|
||
const response = await fetch('/api/cartazes/decompose-scene', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||
},
|
||
body: JSON.stringify({ tema: prompt, titulo })
|
||
});
|
||
|
||
clearInterval(statusInterval);
|
||
|
||
if (!response.ok) {
|
||
const errData = await response.json().catch(() => ({}));
|
||
throw new Error(errData.error || 'Erro na geração da cena.');
|
||
}
|
||
|
||
const data = await response.json();
|
||
if (!data.success) throw new Error('Resposta inválida do servidor.');
|
||
|
||
if (sceneDecomposeStatus) sceneDecomposeStatus.textContent = 'Montando as camadas no canvas...';
|
||
|
||
// Limpar canvas antes de montar (remover outros items de cena se houver)
|
||
const oldLayers = cartazPrintableArea.querySelectorAll('.canvas-scene-layer');
|
||
oldLayers.forEach(l => l.remove());
|
||
|
||
// Adicionar background
|
||
if (data.backgroundUrl) {
|
||
addSceneLayerToCanvas({ url: data.backgroundUrl, name: 'Cenário de Fundo', isBackground: true });
|
||
}
|
||
|
||
// Adicionar cada elemento com posição escalonada
|
||
if (Array.isArray(data.layers)) {
|
||
data.layers.forEach((layer, idx) => {
|
||
if (layer.url) {
|
||
const spread = [
|
||
{ left: 10, top: 30 },
|
||
{ left: 38, top: 35 },
|
||
{ left: 62, top: 28 },
|
||
{ left: 25, top: 55 }
|
||
];
|
||
const pos = spread[idx % spread.length];
|
||
addSceneLayerToCanvas({
|
||
url: layer.url,
|
||
name: layer.name || `Elemento ${idx + 1}`,
|
||
isBackground: false,
|
||
left: pos.left,
|
||
top: pos.top,
|
||
width: 28,
|
||
height: 28,
|
||
zOffset: idx + 1
|
||
});
|
||
}
|
||
});
|
||
|
||
// Renderizar lista de camadas no painel
|
||
renderSceneLayers(data.layers);
|
||
if (sceneLayersList) sceneLayersList.style.display = 'flex';
|
||
}
|
||
|
||
showToast('🎬 Cena montada em camadas! Clique nos elementos para mover, girar e redimensionar.', 'success');
|
||
|
||
} catch (err) {
|
||
clearInterval(statusInterval);
|
||
console.error('[Decompose Scene]', err);
|
||
showToast('Erro ao criar a cena em camadas: ' + err.message, 'error');
|
||
} finally {
|
||
btnDecomposeScene.disabled = false;
|
||
btnDecomposeScene.style.opacity = '1';
|
||
if (sceneDecomposeLoader) sceneDecomposeLoader.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
function enterCanvasMode() {
|
||
canvasModeActive = true;
|
||
if (btnCanvasModeToggle) {
|
||
btnCanvasModeToggle.textContent = '💾 Salvar Canvas';
|
||
btnCanvasModeToggle.style.background = '#10a37f';
|
||
btnCanvasModeToggle.style.color = 'white';
|
||
}
|
||
if (canvasControlPanel) canvasControlPanel.style.display = 'flex';
|
||
|
||
// Alternar painéis laterais
|
||
const configPanel = document.getElementById('cartazLeftConfigPanel');
|
||
const canvasPanel = document.getElementById('cartazLeftCanvasPanel');
|
||
if (configPanel) configPanel.style.display = 'none';
|
||
if (canvasPanel) canvasPanel.style.display = 'flex';
|
||
|
||
// Iniciar na aba correspondente de assets
|
||
selectAssetTab(activeAssetTab);
|
||
|
||
cartazPrintableArea.style.position = 'relative';
|
||
const rectParent = cartazPrintableArea.getBoundingClientRect();
|
||
|
||
const items = [];
|
||
if (cartazPreviewTitle) items.push(cartazPreviewTitle);
|
||
if (cartazPreviewImageContainer && cartazPreviewImageContainer.style.display !== 'none') {
|
||
items.push(cartazPreviewImageContainer);
|
||
}
|
||
|
||
const cards = Array.from(cartazPreviewContent.children);
|
||
cards.forEach(card => {
|
||
items.push(card);
|
||
});
|
||
|
||
const clientRects = items.map(item => item.getBoundingClientRect());
|
||
|
||
items.forEach((item, index) => {
|
||
const rect = clientRects[index];
|
||
|
||
const leftPercent = ((rect.left - rectParent.left) / rectParent.width) * 100;
|
||
const topPercent = ((rect.top - rectParent.top) / rectParent.height) * 100;
|
||
const widthPercent = (rect.width / rectParent.width) * 100;
|
||
const heightPercent = (rect.height / rectParent.height) * 100;
|
||
|
||
item.classList.add('canvas-item');
|
||
item.style.position = 'absolute';
|
||
item.style.left = `${leftPercent}%`;
|
||
item.style.top = `${topPercent}%`;
|
||
item.style.width = `${widthPercent}%`;
|
||
item.style.height = `${heightPercent}%`;
|
||
item.style.margin = '0';
|
||
|
||
item.setAttribute('contenteditable', 'true');
|
||
|
||
if (!item.querySelector('.canvas-resize-handle')) {
|
||
const handle = document.createElement('div');
|
||
handle.className = 'canvas-resize-handle';
|
||
item.appendChild(handle);
|
||
}
|
||
|
||
if (item.parentNode === cartazPreviewContent) {
|
||
cartazPrintableArea.appendChild(item);
|
||
}
|
||
|
||
setupCanvasItemEvents(item);
|
||
});
|
||
|
||
cartazPreviewContent.style.display = 'none';
|
||
showToast('Modo Design Canvas ativado! Adicione elementos e ajuste o mural livremente.', 'info');
|
||
}
|
||
|
||
function exitCanvasMode() {
|
||
canvasModeActive = false;
|
||
if (btnCanvasModeToggle) {
|
||
btnCanvasModeToggle.textContent = '🎨 Design Canvas';
|
||
btnCanvasModeToggle.style.background = 'transparent';
|
||
btnCanvasModeToggle.style.color = 'var(--text-secondary)';
|
||
}
|
||
if (canvasControlPanel) canvasControlPanel.style.display = 'none';
|
||
|
||
// Alternar painéis de volta
|
||
const configPanel = document.getElementById('cartazLeftConfigPanel');
|
||
const canvasPanel = document.getElementById('cartazLeftCanvasPanel');
|
||
if (configPanel) configPanel.style.display = 'flex';
|
||
if (canvasPanel) canvasPanel.style.display = 'none';
|
||
|
||
if (selectedCanvasItem) {
|
||
selectedCanvasItem.classList.remove('active-item');
|
||
selectedCanvasItem = null;
|
||
}
|
||
|
||
const items = cartazPrintableArea.querySelectorAll('.canvas-item');
|
||
items.forEach(item => {
|
||
item.removeAttribute('contenteditable');
|
||
});
|
||
|
||
// Limpar o HTML para persistência (remover seleções/handles)
|
||
const tempContainer = document.createElement('div');
|
||
tempContainer.innerHTML = cartazPrintableArea.innerHTML;
|
||
tempContainer.querySelectorAll('.canvas-resize-handle').forEach(h => h.remove());
|
||
tempContainer.querySelectorAll('.canvas-item').forEach(el => {
|
||
el.classList.remove('active-item');
|
||
el.removeAttribute('contenteditable');
|
||
if (el.style.border && el.style.border.includes('dashed')) {
|
||
el.style.border = 'none';
|
||
}
|
||
});
|
||
const cleanHtml = tempContainer.innerHTML;
|
||
|
||
// Persistir alterações de canvas no banco
|
||
if (activeCartazData && activeCartazData.id) {
|
||
fetch(`/api/cartazes/${activeCartazData.id}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||
},
|
||
body: JSON.stringify({
|
||
conteudo: cleanHtml,
|
||
corFundo: cartazBgColor.value,
|
||
corTexto: cartazTextColor.value
|
||
})
|
||
}).then(res => {
|
||
if (res.ok) {
|
||
showToast('Layout do Canvas salvo no servidor!', 'success');
|
||
loadCartazesHistory();
|
||
} else {
|
||
showToast('Erro ao salvar layout no servidor.', 'error');
|
||
}
|
||
}).catch(e => {
|
||
console.error(e);
|
||
showToast('Erro de conexão ao salvar canvas.', 'error');
|
||
});
|
||
} else {
|
||
showToast('Layout salvo temporariamente. Crie o cartaz para salvar definitivamente.', 'warning');
|
||
}
|
||
}
|
||
|
||
function setupCanvasItemEvents(item) {
|
||
item.addEventListener('click', (e) => {
|
||
if (!canvasModeActive) return;
|
||
e.stopPropagation();
|
||
|
||
if (selectedCanvasItem) {
|
||
selectedCanvasItem.classList.remove('active-item');
|
||
}
|
||
selectedCanvasItem = item;
|
||
item.classList.add('active-item');
|
||
|
||
updateToolbarForSelected();
|
||
});
|
||
|
||
// Clique duplo para focar e editar texto
|
||
item.addEventListener('dblclick', (e) => {
|
||
if (!canvasModeActive) return;
|
||
if (item.querySelector('img')) return; // Evita editar imagens
|
||
|
||
e.stopPropagation();
|
||
item.setAttribute('contenteditable', 'true');
|
||
item.focus();
|
||
|
||
// Coloca o cursor de digitação no texto
|
||
const range = document.createRange();
|
||
range.selectNodeContents(item);
|
||
range.collapse(false);
|
||
const sel = window.getSelection();
|
||
sel.removeAllRanges();
|
||
sel.addRange(range);
|
||
});
|
||
|
||
item.addEventListener('mousedown', (e) => {
|
||
if (!canvasModeActive) return;
|
||
if (e.target.classList.contains('canvas-resize-handle')) return;
|
||
|
||
// Se o item já estiver focado/editando, permite seleção de texto livre sem arrastar
|
||
if (item.getAttribute('contenteditable') === 'true' && document.activeElement === item) {
|
||
return;
|
||
}
|
||
|
||
e.preventDefault();
|
||
selectedCanvasItem = item;
|
||
document.querySelectorAll('.canvas-item').forEach(el => el.classList.remove('active-item'));
|
||
item.classList.add('active-item');
|
||
updateToolbarForSelected();
|
||
|
||
isDraggingCanvas = true;
|
||
dragStartX = e.clientX;
|
||
dragStartY = e.clientY;
|
||
|
||
const parentRect = cartazPrintableArea.getBoundingClientRect();
|
||
const itemRect = item.getBoundingClientRect();
|
||
|
||
dragStartLeft = ((itemRect.left - parentRect.left) / parentRect.width) * 100;
|
||
dragStartTop = ((itemRect.top - parentRect.top) / parentRect.height) * 100;
|
||
|
||
// Previne o highlight azul do browser durante o drag
|
||
document.body.style.userSelect = 'none';
|
||
|
||
document.addEventListener('mousemove', handleCanvasDrag);
|
||
document.addEventListener('mouseup', stopCanvasDrag);
|
||
});
|
||
|
||
item.addEventListener('touchstart', (e) => {
|
||
if (!canvasModeActive) return;
|
||
if (e.target.classList.contains('canvas-resize-handle')) return;
|
||
|
||
if (item.getAttribute('contenteditable') === 'true' && document.activeElement === item) {
|
||
return;
|
||
}
|
||
|
||
selectedCanvasItem = item;
|
||
document.querySelectorAll('.canvas-item').forEach(el => el.classList.remove('active-item'));
|
||
item.classList.add('active-item');
|
||
updateToolbarForSelected();
|
||
|
||
isDraggingCanvas = true;
|
||
const touch = e.touches[0];
|
||
dragStartX = touch.clientX;
|
||
dragStartY = touch.clientY;
|
||
|
||
const parentRect = cartazPrintableArea.getBoundingClientRect();
|
||
const itemRect = item.getBoundingClientRect();
|
||
|
||
dragStartLeft = ((itemRect.left - parentRect.left) / parentRect.width) * 100;
|
||
dragStartTop = ((itemRect.top - parentRect.top) / parentRect.height) * 100;
|
||
|
||
document.addEventListener('touchmove', handleCanvasTouchDrag, { passive: false });
|
||
document.addEventListener('touchend', stopCanvasDrag);
|
||
});
|
||
|
||
const handle = item.querySelector('.canvas-resize-handle');
|
||
if (handle) {
|
||
handle.addEventListener('mousedown', (e) => {
|
||
if (!canvasModeActive) return;
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
|
||
// Garante que o item clicado seja o selecionado
|
||
document.querySelectorAll('.canvas-item').forEach(el => el.classList.remove('active-item'));
|
||
selectedCanvasItem = item;
|
||
item.classList.add('active-item');
|
||
updateToolbarForSelected();
|
||
|
||
isResizingCanvas = true;
|
||
dragStartX = e.clientX;
|
||
dragStartY = e.clientY;
|
||
|
||
const parentRect = cartazPrintableArea.getBoundingClientRect();
|
||
const itemRect = item.getBoundingClientRect();
|
||
|
||
dragStartWidth = (itemRect.width / parentRect.width) * 100;
|
||
dragStartHeight = (itemRect.height / parentRect.height) * 100;
|
||
|
||
// Previne seleção de texto durante o drag de resize
|
||
document.body.style.userSelect = 'none';
|
||
|
||
document.addEventListener('mousemove', handleCanvasResize);
|
||
document.addEventListener('mouseup', stopCanvasResize);
|
||
});
|
||
|
||
handle.addEventListener('touchstart', (e) => {
|
||
if (!canvasModeActive) return;
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
|
||
document.querySelectorAll('.canvas-item').forEach(el => el.classList.remove('active-item'));
|
||
selectedCanvasItem = item;
|
||
item.classList.add('active-item');
|
||
updateToolbarForSelected();
|
||
|
||
isResizingCanvas = true;
|
||
const touch = e.touches[0];
|
||
dragStartX = touch.clientX;
|
||
dragStartY = touch.clientY;
|
||
|
||
const parentRect = cartazPrintableArea.getBoundingClientRect();
|
||
const itemRect = item.getBoundingClientRect();
|
||
|
||
dragStartWidth = (itemRect.width / parentRect.width) * 100;
|
||
dragStartHeight = (itemRect.height / parentRect.height) * 100;
|
||
|
||
document.addEventListener('touchmove', handleCanvasTouchResize, { passive: false });
|
||
document.addEventListener('touchend', stopCanvasResize);
|
||
});
|
||
}
|
||
}
|
||
|
||
function handleCanvasDrag(e) {
|
||
if (!isDraggingCanvas || !selectedCanvasItem) return;
|
||
const dx = e.clientX - dragStartX;
|
||
const dy = e.clientY - dragStartY;
|
||
const parentRect = cartazPrintableArea.getBoundingClientRect();
|
||
const dxPercent = (dx / parentRect.width) * 100;
|
||
const dyPercent = (dy / parentRect.height) * 100;
|
||
|
||
let newLeft = dragStartLeft + dxPercent;
|
||
let newTop = dragStartTop + dyPercent;
|
||
|
||
const itemRect = selectedCanvasItem.getBoundingClientRect();
|
||
const itemWPercent = (itemRect.width / parentRect.width) * 100;
|
||
const itemHPercent = (itemRect.height / parentRect.height) * 100;
|
||
|
||
if (newLeft < 0) newLeft = 0;
|
||
if (newTop < 0) newTop = 0;
|
||
if (newLeft + itemWPercent > 100) newLeft = 100 - itemWPercent;
|
||
if (newTop + itemHPercent > 100) newTop = 100 - itemHPercent;
|
||
|
||
selectedCanvasItem.style.left = `${newLeft}%`;
|
||
selectedCanvasItem.style.top = `${newTop}%`;
|
||
}
|
||
|
||
function handleCanvasTouchDrag(e) {
|
||
if (!isDraggingCanvas || !selectedCanvasItem) return;
|
||
e.preventDefault();
|
||
const touch = e.touches[0];
|
||
const dx = touch.clientX - dragStartX;
|
||
const dy = touch.clientY - dragStartY;
|
||
const parentRect = cartazPrintableArea.getBoundingClientRect();
|
||
const dxPercent = (dx / parentRect.width) * 100;
|
||
const dyPercent = (dy / parentRect.height) * 100;
|
||
|
||
let newLeft = dragStartLeft + dxPercent;
|
||
let newTop = dragStartTop + dyPercent;
|
||
|
||
const itemRect = selectedCanvasItem.getBoundingClientRect();
|
||
const itemWPercent = (itemRect.width / parentRect.width) * 100;
|
||
const itemHPercent = (itemRect.height / parentRect.height) * 100;
|
||
|
||
if (newLeft < 0) newLeft = 0;
|
||
if (newTop < 0) newTop = 0;
|
||
if (newLeft + itemWPercent > 100) newLeft = 100 - itemWPercent;
|
||
if (newTop + itemHPercent > 100) newTop = 100 - itemHPercent;
|
||
|
||
selectedCanvasItem.style.left = `${newLeft}%`;
|
||
selectedCanvasItem.style.top = `${newTop}%`;
|
||
}
|
||
|
||
function stopCanvasDrag() {
|
||
isDraggingCanvas = false;
|
||
document.body.style.userSelect = ''; // Restaura seleção de texto
|
||
document.removeEventListener('mousemove', handleCanvasDrag);
|
||
document.removeEventListener('mouseup', stopCanvasDrag);
|
||
document.removeEventListener('touchmove', handleCanvasTouchDrag);
|
||
document.removeEventListener('touchend', stopCanvasDrag);
|
||
}
|
||
|
||
function handleCanvasResize(e) {
|
||
if (!isResizingCanvas || !selectedCanvasItem) return;
|
||
const dx = e.clientX - dragStartX;
|
||
const dy = e.clientY - dragStartY;
|
||
const parentRect = cartazPrintableArea.getBoundingClientRect();
|
||
const dxPercent = (dx / parentRect.width) * 100;
|
||
const dyPercent = (dy / parentRect.height) * 100;
|
||
|
||
let newWidth = dragStartWidth + dxPercent;
|
||
let newHeight = dragStartHeight + dyPercent;
|
||
|
||
if (newWidth < 10) newWidth = 10;
|
||
if (newHeight < 5) newHeight = 5;
|
||
|
||
const itemLeft = parseFloat(selectedCanvasItem.style.left) || 0;
|
||
const itemTop = parseFloat(selectedCanvasItem.style.top) || 0;
|
||
|
||
if (itemLeft + newWidth > 100) newWidth = 100 - itemLeft;
|
||
if (itemTop + newHeight > 100) newHeight = 100 - itemTop;
|
||
|
||
selectedCanvasItem.style.width = `${newWidth}%`;
|
||
selectedCanvasItem.style.height = `${newHeight}%`;
|
||
}
|
||
|
||
function handleCanvasTouchResize(e) {
|
||
if (!isResizingCanvas || !selectedCanvasItem) return;
|
||
e.preventDefault();
|
||
const touch = e.touches[0];
|
||
const dx = touch.clientX - dragStartX;
|
||
const dy = touch.clientY - dragStartY;
|
||
const parentRect = cartazPrintableArea.getBoundingClientRect();
|
||
const dxPercent = (dx / parentRect.width) * 100;
|
||
const dyPercent = (dy / parentRect.height) * 100;
|
||
|
||
let newWidth = dragStartWidth + dxPercent;
|
||
let newHeight = dragStartHeight + dyPercent;
|
||
|
||
if (newWidth < 10) newWidth = 10;
|
||
if (newHeight < 5) newHeight = 5;
|
||
|
||
const itemLeft = parseFloat(selectedCanvasItem.style.left) || 0;
|
||
const itemTop = parseFloat(selectedCanvasItem.style.top) || 0;
|
||
|
||
if (itemLeft + newWidth > 100) newWidth = 100 - itemLeft;
|
||
if (itemTop + newHeight > 100) newHeight = 100 - itemTop;
|
||
|
||
selectedCanvasItem.style.width = `${newWidth}%`;
|
||
selectedCanvasItem.style.height = `${newHeight}%`;
|
||
}
|
||
|
||
function stopCanvasResize() {
|
||
isResizingCanvas = false;
|
||
document.body.style.userSelect = ''; // Restaura seleção de texto
|
||
document.removeEventListener('mousemove', handleCanvasResize);
|
||
document.removeEventListener('mouseup', stopCanvasResize);
|
||
document.removeEventListener('touchmove', handleCanvasTouchResize);
|
||
document.removeEventListener('touchend', stopCanvasResize);
|
||
}
|
||
|
||
function rgbToHex(rgb) {
|
||
if (!rgb || rgb === 'transparent') return '#ffffff';
|
||
if (rgb.startsWith('#')) return rgb;
|
||
const match = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);
|
||
if (!match) return '#ffffff';
|
||
const r = parseInt(match[1]);
|
||
const g = parseInt(match[2]);
|
||
const b = parseInt(match[3]);
|
||
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
||
}
|
||
|
||
function updateToolbarForSelected() {
|
||
if (!selectedCanvasItem) return;
|
||
|
||
const ff = window.getComputedStyle(selectedCanvasItem).fontFamily.replace(/['"]/g, '').split(',')[0].trim();
|
||
if (canvasFontFamily) canvasFontFamily.value = ff;
|
||
|
||
const fs = parseFloat(window.getComputedStyle(selectedCanvasItem).fontSize);
|
||
if (canvasFontSize) canvasFontSize.value = Math.round(fs);
|
||
|
||
const col = rgbToHex(window.getComputedStyle(selectedCanvasItem).color);
|
||
if (canvasTextColor) canvasTextColor.value = col;
|
||
|
||
const bg = rgbToHex(window.getComputedStyle(selectedCanvasItem).backgroundColor);
|
||
if (canvasBgColorInput) canvasBgColorInput.value = bg;
|
||
|
||
const rot = selectedCanvasItem.getAttribute('data-rotation') || '0';
|
||
if (canvasRotation) canvasRotation.value = rot;
|
||
|
||
const op = window.getComputedStyle(selectedCanvasItem).opacity || '1';
|
||
if (canvasOpacity) canvasOpacity.value = Math.round(parseFloat(op) * 100);
|
||
}
|
||
|
||
if (canvasFontFamily) {
|
||
canvasFontFamily.addEventListener('change', () => {
|
||
if (selectedCanvasItem) selectedCanvasItem.style.fontFamily = canvasFontFamily.value;
|
||
});
|
||
}
|
||
if (canvasFontSize) {
|
||
canvasFontSize.addEventListener('input', () => {
|
||
if (selectedCanvasItem) selectedCanvasItem.style.fontSize = `${canvasFontSize.value}px`;
|
||
});
|
||
}
|
||
if (canvasTextColor) {
|
||
canvasTextColor.addEventListener('input', () => {
|
||
if (selectedCanvasItem) selectedCanvasItem.style.color = canvasTextColor.value;
|
||
});
|
||
}
|
||
if (canvasBgColorInput) {
|
||
canvasBgColorInput.addEventListener('input', () => {
|
||
if (selectedCanvasItem) selectedCanvasItem.style.backgroundColor = canvasBgColorInput.value;
|
||
});
|
||
}
|
||
if (canvasRotation) {
|
||
canvasRotation.addEventListener('input', () => {
|
||
if (selectedCanvasItem) {
|
||
const val = canvasRotation.value;
|
||
selectedCanvasItem.style.transform = `rotate(${val}deg)`;
|
||
selectedCanvasItem.setAttribute('data-rotation', val);
|
||
}
|
||
});
|
||
}
|
||
if (canvasOpacity) {
|
||
canvasOpacity.addEventListener('input', () => {
|
||
if (selectedCanvasItem) {
|
||
const val = canvasOpacity.value;
|
||
selectedCanvasItem.style.opacity = parseFloat(val) / 100;
|
||
}
|
||
});
|
||
}
|
||
if (btnCanvasZIndexUp) {
|
||
btnCanvasZIndexUp.addEventListener('click', () => {
|
||
if (selectedCanvasItem) {
|
||
const currentZ = parseInt(selectedCanvasItem.style.zIndex) || 1;
|
||
selectedCanvasItem.style.zIndex = currentZ + 1;
|
||
}
|
||
});
|
||
}
|
||
if (btnCanvasZIndexDown) {
|
||
btnCanvasZIndexDown.addEventListener('click', () => {
|
||
if (selectedCanvasItem) {
|
||
const currentZ = parseInt(selectedCanvasItem.style.zIndex) || 1;
|
||
selectedCanvasItem.style.zIndex = Math.max(1, currentZ - 1);
|
||
}
|
||
});
|
||
}
|
||
if (btnCanvasDelete) {
|
||
btnCanvasDelete.addEventListener('click', () => {
|
||
if (selectedCanvasItem) {
|
||
selectedCanvasItem.remove();
|
||
selectedCanvasItem = null;
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnCanvasAddText) {
|
||
btnCanvasAddText.addEventListener('click', () => {
|
||
if (!canvasModeActive) return;
|
||
const textItem = document.createElement('div');
|
||
textItem.className = 'canvas-item';
|
||
textItem.innerHTML = 'Novo Texto (Clique duplo para editar)';
|
||
textItem.style.left = '10%';
|
||
textItem.style.top = '15%';
|
||
textItem.style.width = '50%';
|
||
textItem.style.height = '8%';
|
||
textItem.style.fontSize = '20px';
|
||
textItem.style.color = cartazTextColor.value || '#2d3748';
|
||
textItem.style.fontFamily = cartazFontSelect.value || 'Fredoka';
|
||
textItem.setAttribute('contenteditable', 'true');
|
||
|
||
const handle = document.createElement('div');
|
||
handle.className = 'canvas-resize-handle';
|
||
textItem.appendChild(handle);
|
||
|
||
cartazPrintableArea.appendChild(textItem);
|
||
setupCanvasItemEvents(textItem);
|
||
textItem.click();
|
||
});
|
||
}
|
||
|
||
if (btnCanvasAddCard) {
|
||
btnCanvasAddCard.addEventListener('click', () => {
|
||
if (!canvasModeActive) return;
|
||
const cardItem = document.createElement('div');
|
||
cardItem.className = 'canvas-item';
|
||
cardItem.style.background = 'rgba(255, 255, 255, 0.4)';
|
||
cardItem.style.border = '2px dashed ' + (cartazTextColor.value || '#2d3748');
|
||
cardItem.style.borderRadius = '12px';
|
||
cardItem.style.left = '20%';
|
||
cardItem.style.top = '25%';
|
||
cardItem.style.width = '45%';
|
||
cardItem.style.height = '20%';
|
||
cardItem.style.padding = '15px';
|
||
cardItem.innerHTML = '<h3>Novo Card</h3><p>Edite este texto...</p>';
|
||
cardItem.style.fontFamily = cartazFontSelect.value || 'Fredoka';
|
||
cardItem.setAttribute('contenteditable', 'true');
|
||
|
||
const handle = document.createElement('div');
|
||
handle.className = 'canvas-resize-handle';
|
||
cardItem.appendChild(handle);
|
||
|
||
cartazPrintableArea.appendChild(cardItem);
|
||
setupCanvasItemEvents(cardItem);
|
||
cardItem.click();
|
||
});
|
||
}
|
||
|
||
if (btnCanvasModeToggle) {
|
||
btnCanvasModeToggle.addEventListener('click', () => {
|
||
if (canvasModeActive) {
|
||
exitCanvasMode();
|
||
} else {
|
||
if (cartazEditArea && cartazEditArea.style.display !== 'none') {
|
||
btnEditCartazToggle.click();
|
||
}
|
||
enterCanvasMode();
|
||
}
|
||
});
|
||
}
|
||
|
||
if (cartazPrintableArea) {
|
||
cartazPrintableArea.addEventListener('click', (e) => {
|
||
if (!canvasModeActive) return;
|
||
if (e.target === cartazPrintableArea) {
|
||
if (selectedCanvasItem) {
|
||
selectedCanvasItem.classList.remove('active-item');
|
||
selectedCanvasItem = null;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function selectAssetTab(tab) {
|
||
activeAssetTab = tab;
|
||
[btnTabAssetPostits, btnTabAssetStickers, btnTabAssetFormas].forEach(btn => {
|
||
if (!btn) return;
|
||
const tabIdName = `btnTabAsset${tab.charAt(0).toUpperCase() + tab.slice(1)}s`;
|
||
if (btn.id === tabIdName) {
|
||
btn.classList.add('active');
|
||
btn.style.background = 'rgba(59, 130, 246, 0.1)';
|
||
btn.style.color = 'var(--text-primary)';
|
||
btn.style.fontWeight = 'bold';
|
||
} else {
|
||
btn.classList.remove('active');
|
||
btn.style.background = 'transparent';
|
||
btn.style.color = 'var(--text-secondary)';
|
||
btn.style.fontWeight = 'normal';
|
||
}
|
||
});
|
||
loadCanvasAssets(tab, canvasAssetSearch ? canvasAssetSearch.value : '');
|
||
}
|
||
|
||
if (btnTabAssetPostits) btnTabAssetPostits.addEventListener('click', () => selectAssetTab('postit'));
|
||
if (btnTabAssetStickers) btnTabAssetStickers.addEventListener('click', () => selectAssetTab('sticker'));
|
||
if (btnTabAssetFormas) btnTabAssetFormas.addEventListener('click', () => selectAssetTab('forma'));
|
||
|
||
if (canvasAssetSearch) {
|
||
canvasAssetSearch.addEventListener('input', (e) => {
|
||
loadCanvasAssets(activeAssetTab, e.target.value);
|
||
});
|
||
}
|
||
|
||
async function loadCanvasAssets(tipo, busca = '') {
|
||
if (!canvasAssetsContainer) return;
|
||
canvasAssetsContainer.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: var(--text-secondary); font-size: 0.8rem; padding: 20px;">Carregando...</div>';
|
||
|
||
try {
|
||
const res = await fetch(`/api/biblioteca-assets?tipo=${tipo}&busca=${encodeURIComponent(busca)}`, {
|
||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||
});
|
||
const data = await res.json();
|
||
canvasAssetsContainer.innerHTML = '';
|
||
|
||
if (!data || data.length === 0) {
|
||
canvasAssetsContainer.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: var(--text-muted); font-size: 0.75rem; padding: 20px;">Nenhum item encontrado.</div>';
|
||
return;
|
||
}
|
||
|
||
data.forEach(asset => {
|
||
const item = document.createElement('div');
|
||
item.style.cursor = 'pointer';
|
||
item.style.padding = '8px';
|
||
item.style.borderRadius = '6px';
|
||
item.style.background = 'var(--bg-tertiary)';
|
||
item.style.border = '1px solid var(--border-light)';
|
||
item.style.display = 'flex';
|
||
item.style.flexDirection = 'column';
|
||
item.style.alignItems = 'center';
|
||
item.style.justifyContent = 'center';
|
||
item.style.gap = '6px';
|
||
item.style.transition = 'all 0.2s';
|
||
item.style.aspectRatio = '1';
|
||
item.title = asset.nome;
|
||
|
||
item.addEventListener('mouseenter', () => {
|
||
item.style.transform = 'scale(1.05)';
|
||
item.style.borderColor = 'rgba(59, 130, 246, 0.5)';
|
||
});
|
||
item.addEventListener('mouseleave', () => {
|
||
item.style.transform = 'scale(1)';
|
||
item.style.borderColor = 'var(--border-light)';
|
||
});
|
||
|
||
let previewHtml = '';
|
||
if (asset.tipo === 'postit') {
|
||
previewHtml = `<div style="width: 32px; height: 32px; background: ${asset.url}; border-radius: 4px; box-shadow: 1px 2px 4px rgba(0,0,0,0.15); transform: rotate(-5deg);"></div>`;
|
||
} else if (asset.tipo === 'sticker') {
|
||
previewHtml = `<img src="${asset.url}" style="width: 36px; height: 36px; object-fit: contain;">`;
|
||
} else if (asset.tipo === 'forma') {
|
||
if (asset.url === 'rect') {
|
||
previewHtml = `<div style="width: 36px; height: 24px; border: 2px solid var(--text-primary); border-radius: 2px;"></div>`;
|
||
} else if (asset.url === 'square') {
|
||
previewHtml = `<div style="width: 30px; height: 30px; border: 2px solid var(--text-primary); border-radius: 2px;"></div>`;
|
||
} else if (asset.url === 'circle') {
|
||
previewHtml = `<div style="width: 32px; height: 32px; border: 2px solid var(--text-primary); border-radius: 50%;"></div>`;
|
||
} else if (asset.url === 'ellipse') {
|
||
previewHtml = `<div style="width: 36px; height: 22px; border: 2px solid var(--text-primary); border-radius: 50%;"></div>`;
|
||
} else if (asset.url === 'line') {
|
||
previewHtml = `<div style="width: 36px; height: 2px; background: var(--text-primary); margin: 15px 0;"></div>`;
|
||
} else if (asset.url === 'triangle') {
|
||
previewHtml = `<div style="width: 32px; height: 32px; background: var(--text-primary); clip-path: polygon(50% 0%, 0% 100%, 100% 100%);"></div>`;
|
||
} else if (asset.url === 'star') {
|
||
previewHtml = `<div style="font-size: 24px; color: var(--text-primary); display: flex; align-items: center; justify-content: center; height: 32px;">⭐</div>`;
|
||
} else if (asset.url === 'arrow') {
|
||
previewHtml = `<div style="font-size: 24px; color: var(--text-primary); display: flex; align-items: center; justify-content: center; height: 32px;">➡️</div>`;
|
||
}
|
||
}
|
||
|
||
item.innerHTML = `
|
||
${previewHtml}
|
||
<span style="font-size: 0.65rem; color: var(--text-primary); text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%;">${asset.nome}</span>
|
||
`;
|
||
|
||
item.addEventListener('click', () => addAssetToCanvas(asset));
|
||
canvasAssetsContainer.appendChild(item);
|
||
});
|
||
} catch(err) {
|
||
console.error(err);
|
||
canvasAssetsContainer.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: #ef4444; font-size: 0.75rem; padding: 10px;">Erro ao carregar elementos.</div>';
|
||
}
|
||
}
|
||
|
||
function addAssetToCanvas(asset) {
|
||
if (!canvasModeActive) return;
|
||
|
||
const item = document.createElement('div');
|
||
item.className = 'canvas-item';
|
||
item.style.position = 'absolute';
|
||
item.style.left = '35%';
|
||
item.style.top = '35%';
|
||
item.style.width = '30%';
|
||
item.style.height = '15%';
|
||
item.style.zIndex = '10';
|
||
|
||
if (asset.tipo === 'postit') {
|
||
item.style.background = asset.url;
|
||
item.style.color = '#2d3748';
|
||
item.style.boxShadow = '3px 5px 10px rgba(0,0,0,0.15)';
|
||
item.style.borderRadius = '4px';
|
||
const randomRot = Math.round(Math.random() * 8 - 4);
|
||
item.style.transform = `rotate(${randomRot}deg)`;
|
||
item.setAttribute('data-rotation', randomRot);
|
||
item.style.padding = '12px';
|
||
item.style.fontFamily = "'Caveat', cursive";
|
||
item.style.fontSize = '22px';
|
||
item.innerHTML = 'Anotação...';
|
||
item.setAttribute('contenteditable', 'true');
|
||
item.style.width = '35%';
|
||
item.style.height = '20%';
|
||
} else if (asset.tipo === 'sticker') {
|
||
item.style.background = 'transparent';
|
||
item.innerHTML = `<img src="${asset.url}" style="width: 100%; height: 100%; object-fit: contain; pointer-events: none;">`;
|
||
item.style.width = '20%';
|
||
item.style.height = '20%';
|
||
item.setAttribute('data-rotation', '0');
|
||
} else if (asset.tipo === 'forma') {
|
||
item.style.background = 'transparent';
|
||
item.style.border = '3px solid ' + (cartazTextColor.value || '#2d3748');
|
||
item.setAttribute('data-rotation', '0');
|
||
if (asset.url === 'rect') {
|
||
item.style.borderRadius = '4px';
|
||
} else if (asset.url === 'square') {
|
||
item.style.borderRadius = '4px';
|
||
item.style.width = '20%';
|
||
item.style.height = '20%';
|
||
} else if (asset.url === 'circle' || asset.url === 'ellipse') {
|
||
item.style.borderRadius = '50%';
|
||
item.style.width = '20%';
|
||
item.style.height = '20%';
|
||
} else if (asset.url === 'line') {
|
||
item.style.border = 'none';
|
||
item.style.borderTop = '4px solid ' + (cartazTextColor.value || '#2d3748');
|
||
item.style.height = '10px';
|
||
item.style.width = '40%';
|
||
} else if (asset.url === 'triangle') {
|
||
item.style.border = 'none';
|
||
item.style.background = cartazTextColor.value || '#2d3748';
|
||
item.style.clipPath = 'polygon(50% 0%, 0% 100%, 100% 100%)';
|
||
item.style.width = '20%';
|
||
item.style.height = '20%';
|
||
} else if (asset.url === 'star') {
|
||
item.style.border = 'none';
|
||
item.style.color = cartazTextColor.value || '#2d3748';
|
||
item.style.fontSize = '48px';
|
||
item.style.display = 'flex';
|
||
item.style.alignItems = 'center';
|
||
item.style.justifyContent = 'center';
|
||
item.innerHTML = '⭐';
|
||
item.style.width = '20%';
|
||
item.style.height = '20%';
|
||
} else if (asset.url === 'arrow') {
|
||
item.style.border = 'none';
|
||
item.style.color = cartazTextColor.value || '#2d3748';
|
||
item.style.fontSize = '48px';
|
||
item.style.display = 'flex';
|
||
item.style.alignItems = 'center';
|
||
item.style.justifyContent = 'center';
|
||
item.innerHTML = '➡️';
|
||
item.style.width = '20%';
|
||
item.style.height = '20%';
|
||
}
|
||
}
|
||
|
||
const handle = document.createElement('div');
|
||
handle.className = 'canvas-resize-handle';
|
||
item.appendChild(handle);
|
||
|
||
cartazPrintableArea.appendChild(item);
|
||
setupCanvasItemEvents(item);
|
||
item.click();
|
||
}
|
||
});
|
||
|
||
// ==========================================================================
|
||
// MÓDULOS EXPANDIDOS: DOSSIÊ DO ALUNO, PROTOCOLO DE CRISES, ATAS & STICKERS
|
||
// ==========================================================================
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
|
||
// ------------------------------------------------------------------------
|
||
// 1. DOSSIÊ & LINHA DO TEMPO DO ALUNO
|
||
// ------------------------------------------------------------------------
|
||
const alunoTimelineModal = document.getElementById('alunoTimelineModal');
|
||
const btnCloseAlunoTimeline = document.getElementById('btnCloseAlunoTimeline');
|
||
const timelineAlunoNome = document.getElementById('timelineAlunoNome');
|
||
const timelineAlunoInfo = document.getElementById('timelineAlunoInfo');
|
||
const timelineContentArea = document.getElementById('timelineContentArea');
|
||
const btnExportFichaConselho = document.getElementById('btnExportFichaConselho');
|
||
let currentTimelineAlunoId = null;
|
||
|
||
if (btnCloseAlunoTimeline) btnCloseAlunoTimeline.addEventListener('click', () => alunoTimelineModal.style.display = 'none');
|
||
if (alunoTimelineModal) alunoTimelineModal.addEventListener('click', (e) => { if (e.target === alunoTimelineModal) alunoTimelineModal.style.display = 'none'; });
|
||
|
||
window.viewChildTimeline = async (alunoId) => {
|
||
currentTimelineAlunoId = alunoId;
|
||
alunoTimelineModal.style.display = 'flex';
|
||
timelineContentArea.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando dossiê...</p>';
|
||
|
||
try {
|
||
const res = await fetch(`/api/alunos/${alunoId}/timeline`);
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || 'Erro ao carregar dados');
|
||
|
||
const aluno = data.aluno;
|
||
timelineAlunoNome.textContent = `Dossiê: ${aluno.nome} ${aluno.especial ? '⭐ (Ed. Especial)' : ''}`;
|
||
timelineAlunoInfo.textContent = `Nasc: ${aluno.data_nasc ? new Date(aluno.data_nasc).toLocaleDateString('pt-BR') : 'Não informada'} • Pais: ${aluno.pais || 'Não informados'} • Saúde: ${aluno.observacoes_saude || 'Nenhuma alergia/restrição'}`;
|
||
|
||
let html = '';
|
||
|
||
// Seção Observações
|
||
const obs = data.timeline.observacoes || [];
|
||
html += `<h4 style="color:#f43f5e; margin:10px 0 6px 0; font-size:0.95rem;">📝 Observações Registradas (${obs.length})</h4>`;
|
||
if (obs.length === 0) {
|
||
html += '<p style="font-size:0.8rem; color:var(--text-secondary);">Nenhuma observação registrada ainda.</p>';
|
||
} else {
|
||
obs.forEach(o => {
|
||
html += `
|
||
<div style="background:rgba(244,63,94,0.05); border-left:3px solid #f43f5e; padding:8px 12px; border-radius:4px; font-size:0.82rem; margin-bottom:6px;">
|
||
<div style="display:flex; justify-content:space-between; color:var(--text-secondary); font-size:0.75rem;">
|
||
<strong>📅 ${o.date || ''} ${o.time || ''}</strong>
|
||
${o.tags ? `<span>🏷️ ${o.tags}</span>` : ''}
|
||
</div>
|
||
<p style="margin:4px 0 0 0; color:var(--text-primary);">${escapeHtml(o.report || '')}</p>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
// Seção Stickers & Conquistas
|
||
const stks = data.timeline.stickers || [];
|
||
html += `<h4 style="color:#fbbf24; margin:14px 0 6px 0; font-size:0.95rem;">🌟 Conquistas & Stickers Recebidos (${stks.length})</h4>`;
|
||
if (stks.length === 0) {
|
||
html += '<p style="font-size:0.8rem; color:var(--text-secondary);">Nenhum sticker concedido ainda.</p>';
|
||
} else {
|
||
html += '<div style="display:flex; flex-wrap:wrap; gap:8px;">';
|
||
stks.forEach(s => {
|
||
html += `
|
||
<div style="background:rgba(245,158,11,0.1); border:1px solid rgba(245,158,11,0.3); border-radius:6px; padding:6px 10px; font-size:0.8rem;">
|
||
<span style="font-size:1.1rem;">${s.icone || '🌟'}</span> <strong>${escapeHtml(s.titulo)}</strong>
|
||
<div style="font-size:0.7rem; color:var(--text-secondary);">${escapeHtml(s.categoria)} • ${new Date(s.created_at).toLocaleDateString('pt-BR')}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
// Seção Histórias
|
||
const hists = data.timeline.historias || [];
|
||
if (hists.length > 0) {
|
||
html += `<h4 style="color:#10b981; margin:14px 0 6px 0; font-size:0.95rem;">📖 Histórias em que Participou (${hists.length})</h4>`;
|
||
hists.forEach(h => {
|
||
html += `
|
||
<div style="background:rgba(16,185,129,0.05); border-left:3px solid #10b981; padding:8px 12px; border-radius:4px; font-size:0.82rem; margin-bottom:6px;">
|
||
<strong>${escapeHtml(h.titulo || h.tema)}</strong>
|
||
<div style="font-size:0.72rem; color:var(--text-secondary);">${h.faixa_etaria} • ${new Date(h.created_at).toLocaleDateString('pt-BR')}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
// Seção Ocorrências / Crises
|
||
const crises = data.timeline.crises || [];
|
||
if (crises.length > 0) {
|
||
html += `<h4 style="color:#ef4444; margin:14px 0 6px 0; font-size:0.95rem;">🚨 Registros de Mediação / Acolhimento (${crises.length})</h4>`;
|
||
crises.forEach(cr => {
|
||
html += `
|
||
<div style="background:rgba(239,68,68,0.05); border-left:3px solid #ef4444; padding:8px 12px; border-radius:4px; font-size:0.82rem; margin-bottom:6px;">
|
||
<div style="display:flex; justify-content:space-between; font-size:0.75rem; color:#f87171;">
|
||
<strong>${escapeHtml(cr.tipo_evento)} (${cr.intensidade})</strong>
|
||
<span>${new Date(cr.data_hora).toLocaleString('pt-BR')}</span>
|
||
</div>
|
||
<p style="margin:4px 0 0 0; color:var(--text-primary);">${escapeHtml(cr.descricao)}</p>
|
||
<div style="font-size:0.75rem; color:var(--text-secondary); margin-top:2px;"><strong>Acolhimento:</strong> ${escapeHtml(cr.medidas_tomadas)}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
// Seção Relatórios Semestrais
|
||
const rels = data.timeline.relatorios || [];
|
||
if (rels.length > 0) {
|
||
html += `<h4 style="color:#3b82f6; margin:14px 0 6px 0; font-size:0.95rem;">📄 Relatórios Semestrais Emitidos (${rels.length})</h4>`;
|
||
rels.forEach(r => {
|
||
html += `
|
||
<div style="background:rgba(59,130,246,0.05); border-left:3px solid #3b82f6; padding:8px 12px; border-radius:4px; font-size:0.82rem; margin-bottom:6px;">
|
||
<strong>${r.semestre}º Semestre de ${r.ano_letivo} (${r.faixa_etaria})</strong>
|
||
<p style="margin:4px 0 0 0; color:var(--text-primary); max-height:100px; overflow-y:auto; font-size:0.8rem;">${escapeHtml(r.conteudo_relatorio)}</p>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
timelineContentArea.innerHTML = html;
|
||
} catch (err) {
|
||
timelineContentArea.innerHTML = `<p style="color:#ef4444; text-align:center;">Erro: ${err.message}</p>`;
|
||
}
|
||
};
|
||
|
||
// Exportar Ficha para Conselho de Classe
|
||
if (btnExportFichaConselho) {
|
||
btnExportFichaConselho.addEventListener('click', async () => {
|
||
if (!currentTimelineAlunoId) return;
|
||
try {
|
||
const res = await fetch(`/api/alunos/${currentTimelineAlunoId}/conselho-ficha`);
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || 'Falha ao buscar ficha');
|
||
|
||
const aluno = data.aluno;
|
||
const win = window.open('', '_blank');
|
||
win.document.write(`
|
||
<!DOCTYPE html>
|
||
<html lang="pt-BR">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Ficha de Conselho - ${aluno.nome}</title>
|
||
<style>
|
||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; padding: 40px; color: #1e293b; line-height: 1.6; max-width: 800px; margin: 0 auto; }
|
||
h1 { color: #1e40af; border-bottom: 2px solid #cbd5e1; padding-bottom: 8px; margin-bottom: 16px; font-size: 1.6rem; }
|
||
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 20px; background: #f8fafc; padding: 15px; border-radius: 8px; border: 1px solid #e2e8f0; }
|
||
.btn-p { background: #3b82f6; color: white; border: none; padding: 10px 20px; border-radius: 6px; font-weight: bold; cursor: pointer; float: right; }
|
||
@media print { .btn-p { display: none; } body { padding: 0; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<button class="btn-p" onclick="window.print()">🖨️ Imprimir Ficha</button>
|
||
<h1>📋 Ficha Individual de Acompanhamento / Conselho de Classe</h1>
|
||
<div class="grid">
|
||
<div><strong>Aluno(a):</strong> ${aluno.nome}</div>
|
||
<div><strong>Turma:</strong> ${aluno.turma_nome || 'N/A'}</div>
|
||
<div><strong>Data Nasc:</strong> ${aluno.data_nasc ? new Date(aluno.data_nasc).toLocaleDateString('pt-BR') : 'N/A'}</div>
|
||
<div><strong>Educação Especial:</strong> ${aluno.especial ? 'Sim (' + (aluno.especial_detalhes || '') + ')' : 'Não'}</div>
|
||
<div><strong>Total de Observações:</strong> ${data.totalObservacoes}</div>
|
||
<div><strong>Emissão:</strong> ${data.dataFicha}</div>
|
||
</div>
|
||
<h3>🌟 Conquistas Pedagógicas:</h3>
|
||
<ul>
|
||
${(data.conquistas || []).map(c => `<li><strong>${c.titulo}</strong> (${c.categoria})</li>`).join('') || '<li>Nenhuma conquista pontual registrada.</li>'}
|
||
</ul>
|
||
<div style="margin-top: 50px; display: flex; justify-content: space-between; border-top: 1px solid #cbd5e1; padding-top: 20px;">
|
||
<div style="text-align: center; width: 45%;">___________________________<br>Professora Titular</div>
|
||
<div style="text-align: center; width: 45%;">___________________________<br>Coordenação Pedagógica</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`);
|
||
win.document.close();
|
||
} catch (e) {
|
||
alert('Erro ao gerar ficha: ' + e.message);
|
||
}
|
||
});
|
||
}
|
||
|
||
// ------------------------------------------------------------------------
|
||
// 2. IMPORTAÇÃO CSV DE ALUNOS
|
||
// ------------------------------------------------------------------------
|
||
const csvImportModal = document.getElementById('csvImportModal');
|
||
const btnCloseCsvImport = document.getElementById('btnCloseCsvImport');
|
||
const csvImportTurmaSelect = document.getElementById('csvImportTurmaSelect');
|
||
const csvFileInput = document.getElementById('csvFileInput');
|
||
const csvTextContent = document.getElementById('csvTextContent');
|
||
const btnProcessCsvImport = document.getElementById('btnProcessCsvImport');
|
||
|
||
if (btnCloseCsvImport) btnCloseCsvImport.addEventListener('click', () => csvImportModal.style.display = 'none');
|
||
if (csvImportModal) csvImportModal.addEventListener('click', (e) => { if (e.target === csvImportModal) csvImportModal.style.display = 'none'; });
|
||
|
||
// Botão abrir CSV import (se presente na tela de turmas ou sidebar)
|
||
window.openCsvImportModal = () => {
|
||
csvImportModal.style.display = 'flex';
|
||
if (csvImportTurmaSelect) {
|
||
csvImportTurmaSelect.innerHTML = '';
|
||
(currentTurmas || []).forEach(t => {
|
||
const opt = document.createElement('option');
|
||
opt.value = t.id;
|
||
opt.textContent = t.nome;
|
||
csvImportTurmaSelect.appendChild(opt);
|
||
});
|
||
}
|
||
};
|
||
|
||
if (csvFileInput) {
|
||
csvFileInput.addEventListener('change', (e) => {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
const reader = new FileReader();
|
||
reader.onload = (evt) => {
|
||
if (csvTextContent) csvTextContent.value = evt.target.result;
|
||
};
|
||
reader.readAsText(file, 'UTF-8');
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnProcessCsvImport) {
|
||
btnProcessCsvImport.addEventListener('click', async () => {
|
||
const turmaId = csvImportTurmaSelect ? csvImportTurmaSelect.value : null;
|
||
const csv = csvTextContent ? csvTextContent.value.trim() : '';
|
||
|
||
if (!turmaId || !csv) {
|
||
alert('Por favor, selecione a turma e forneça o conteúdo CSV.');
|
||
return;
|
||
}
|
||
|
||
btnProcessCsvImport.disabled = true;
|
||
btnProcessCsvImport.textContent = '⏳ Importando...';
|
||
|
||
try {
|
||
const res = await fetch('/api/alunos/import-csv', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ turma_id: turmaId, csv_content: csv })
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || 'Erro na importação');
|
||
|
||
alert(`✅ Sucesso! ${data.count} alunos foram importados para a turma.`);
|
||
csvImportModal.style.display = 'none';
|
||
if (typeof fetchAlunos === 'function') await fetchAlunos();
|
||
if (typeof renderAlunosList === 'function') renderAlunosList();
|
||
} catch (err) {
|
||
alert('Erro ao importar CSV: ' + err.message);
|
||
} finally {
|
||
btnProcessCsvImport.disabled = false;
|
||
btnProcessCsvImport.textContent = '📥 Importar Alunos';
|
||
}
|
||
});
|
||
}
|
||
|
||
// ------------------------------------------------------------------------
|
||
// 3. PROTOCOLO DE CRISES & MEDIAÇÃO COMPORTAMENTAL
|
||
// ------------------------------------------------------------------------
|
||
const btnProtocoloCrise = document.getElementById('btnProtocoloCrise');
|
||
const criseModal = document.getElementById('criseModal');
|
||
const btnCloseCriseModal = document.getElementById('btnCloseCriseModal');
|
||
const btnOpenCrisesHistory = document.getElementById('btnOpenCrisesHistory');
|
||
const criseHistoryModal = document.getElementById('criseHistoryModal');
|
||
const btnCloseCriseHistory = document.getElementById('btnCloseCriseHistory');
|
||
const criseTurmaSelect = document.getElementById('criseTurmaSelect');
|
||
const criseAlunoInput = document.getElementById('criseAlunoInput');
|
||
const criseTipoSelect = document.getElementById('criseTipoSelect');
|
||
const criseIntensidadeSelect = document.getElementById('criseIntensidadeSelect');
|
||
const criseDescricaoInput = document.getElementById('criseDescricaoInput');
|
||
const criseMedidasInput = document.getElementById('criseMedidasInput');
|
||
const btnSalvarCrise = document.getElementById('btnSalvarCrise');
|
||
const criseWhatsappOutput = document.getElementById('criseWhatsappOutput');
|
||
const btnCopyCriseWhatsapp = document.getElementById('btnCopyCriseWhatsapp');
|
||
const btnSendWhatsappDirect = document.getElementById('btnSendWhatsappDirect');
|
||
const criseHistoryList = document.getElementById('criseHistoryList');
|
||
|
||
function populateCriseTurmas() {
|
||
if (!criseTurmaSelect) return;
|
||
criseTurmaSelect.innerHTML = '<option value="">-- Selecionar Turma --</option>';
|
||
(currentTurmas || []).forEach(t => {
|
||
const opt = document.createElement('option');
|
||
opt.value = t.id;
|
||
opt.textContent = t.nome;
|
||
criseTurmaSelect.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
if (btnProtocoloCrise) {
|
||
btnProtocoloCrise.addEventListener('click', () => {
|
||
criseModal.style.display = 'flex';
|
||
populateCriseTurmas();
|
||
});
|
||
}
|
||
if (btnCloseCriseModal) btnCloseCriseModal.addEventListener('click', () => criseModal.style.display = 'none');
|
||
if (criseModal) criseModal.addEventListener('click', (e) => { if (e.target === criseModal) criseModal.style.display = 'none'; });
|
||
|
||
if (btnOpenCrisesHistory) {
|
||
btnOpenCrisesHistory.addEventListener('click', async () => {
|
||
criseHistoryModal.style.display = 'flex';
|
||
if (!criseHistoryList) return;
|
||
criseHistoryList.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando...</p>';
|
||
try {
|
||
const res = await fetch('/api/crises/list');
|
||
const data = await res.json();
|
||
if (data.length === 0) {
|
||
criseHistoryList.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Nenhuma ocorrência registrada.</p>';
|
||
return;
|
||
}
|
||
criseHistoryList.innerHTML = data.map(c => `
|
||
<div style="background:rgba(255,255,255,0.02); border:1px solid var(--border-light); padding:12px; border-radius:8px; display:flex; flex-direction:column; gap:4px; position:relative;">
|
||
<div style="display:flex; justify-content:space-between; padding-right:24px;">
|
||
<strong style="color:#ef4444; font-size:0.9rem;">${escapeHtml(c.aluno_nome)} • ${escapeHtml(c.tipo_evento)}</strong>
|
||
<span style="font-size:0.72rem; color:var(--text-secondary);">${new Date(c.data_hora).toLocaleString('pt-BR')}</span>
|
||
</div>
|
||
<p style="margin:4px 0; font-size:0.82rem; color:var(--text-primary);">${escapeHtml(c.descricao)}</p>
|
||
<div style="font-size:0.75rem; color:var(--text-secondary);"><strong>Medidas:</strong> ${escapeHtml(c.medidas_tomadas)}</div>
|
||
<button onclick="deleteCriseRecord('${c.id}')" style="position:absolute; top:8px; right:8px; background:none; border:none; color:#ef4444; cursor:pointer;" title="Excluir">🗑️</button>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) {
|
||
criseHistoryList.innerHTML = `<p style="color:#ef4444;">Erro ao carregar: ${e.message}</p>`;
|
||
}
|
||
});
|
||
}
|
||
|
||
window.deleteCriseRecord = async (id) => {
|
||
if (confirm('Deseja excluir este registro de mediação?')) {
|
||
await fetch(`/api/crises/${id}`, { method: 'DELETE' });
|
||
if (btnOpenCrisesHistory) btnOpenCrisesHistory.click();
|
||
}
|
||
};
|
||
|
||
if (btnCloseCriseHistory) btnCloseCriseHistory.addEventListener('click', () => criseHistoryModal.style.display = 'none');
|
||
|
||
if (btnSalvarCrise) {
|
||
btnSalvarCrise.addEventListener('click', async () => {
|
||
const alunoNome = criseAlunoInput ? criseAlunoInput.value.trim() : '';
|
||
const tipo = criseTipoSelect ? criseTipoSelect.value : '';
|
||
const intensidade = criseIntensidadeSelect ? criseIntensidadeSelect.value : 'Moderada';
|
||
const desc = criseDescricaoInput ? criseDescricaoInput.value.trim() : '';
|
||
const medidas = criseMedidasInput ? criseMedidasInput.value.trim() : '';
|
||
const turmaId = criseTurmaSelect ? criseTurmaSelect.value : null;
|
||
|
||
if (!alunoNome || !desc) {
|
||
alert('Por favor, informe o nome da criança e a descrição do ocorrido.');
|
||
return;
|
||
}
|
||
|
||
btnSalvarCrise.disabled = true;
|
||
btnSalvarCrise.innerHTML = '⏳ Gerando Registro e Mensagem...';
|
||
|
||
try {
|
||
const res = await fetch('/api/crises/register', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
aluno_nome: alunoNome,
|
||
turma_id: turmaId,
|
||
tipo_evento: tipo,
|
||
intensidade: intensidade,
|
||
descricao: desc,
|
||
medidas_tomadas: medidas
|
||
})
|
||
});
|
||
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || 'Falha ao registrar ocorrência');
|
||
|
||
if (criseWhatsappOutput) criseWhatsappOutput.textContent = data.mensagemWhatsapp || '';
|
||
if (btnSendWhatsappDirect) {
|
||
btnSendWhatsappDirect.href = `https://api.whatsapp.com/send?text=${encodeURIComponent(data.mensagemWhatsapp || '')}`;
|
||
btnSendWhatsappDirect.style.display = 'block';
|
||
}
|
||
alert('✅ Protocolo registrado com sucesso! Mensagem para os pais gerada.');
|
||
} catch (err) {
|
||
alert('Erro ao registrar crise: ' + err.message);
|
||
} finally {
|
||
btnSalvarCrise.disabled = false;
|
||
btnSalvarCrise.innerHTML = '🛡️ Registrar & Gerar Mensagem WhatsApp';
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnCopyCriseWhatsapp) {
|
||
btnCopyCriseWhatsapp.addEventListener('click', () => {
|
||
if (criseWhatsappOutput && criseWhatsappOutput.textContent) {
|
||
navigator.clipboard.writeText(criseWhatsappOutput.textContent);
|
||
alert('Mensagem copiada para a área de transferência!');
|
||
}
|
||
});
|
||
}
|
||
|
||
// ------------------------------------------------------------------------
|
||
// 4. ATAS DE REUNIÃO & CONSELHO DE CLASSE
|
||
// ------------------------------------------------------------------------
|
||
const btnAtasReuniao = document.getElementById('btnAtasReuniao');
|
||
const ataModal = document.getElementById('ataModal');
|
||
const btnCloseAtaModal = document.getElementById('btnCloseAtaModal');
|
||
const btnOpenAtasHistory = document.getElementById('btnOpenAtasHistory');
|
||
const atasHistoryModal = document.getElementById('atasHistoryModal');
|
||
const btnCloseAtasHistory = document.getElementById('btnCloseAtasHistory');
|
||
const ataTurmaSelect = document.getElementById('ataTurmaSelect');
|
||
const ataTipoSelect = document.getElementById('ataTipoSelect');
|
||
const ataTituloInput = document.getElementById('ataTituloInput');
|
||
const ataParticipantesInput = document.getElementById('ataParticipantesInput');
|
||
const ataPautaInput = document.getElementById('ataPautaInput');
|
||
const ataDiscussoesInput = document.getElementById('ataDiscussoesInput');
|
||
const ataDeliberacoesInput = document.getElementById('ataDeliberacoesInput');
|
||
const btnGerarAta = document.getElementById('btnGerarAta');
|
||
const ataOutputText = document.getElementById('ataOutputText');
|
||
const btnCopyAta = document.getElementById('btnCopyAta');
|
||
const btnPrintAtaPdf = document.getElementById('btnPrintAtaPdf');
|
||
const atasHistoryList = document.getElementById('atasHistoryList');
|
||
|
||
function populateAtaTurmas() {
|
||
if (!ataTurmaSelect) return;
|
||
ataTurmaSelect.innerHTML = '<option value="">-- Geral / Toda a Escola --</option>';
|
||
(currentTurmas || []).forEach(t => {
|
||
const opt = document.createElement('option');
|
||
opt.value = t.id;
|
||
opt.textContent = t.nome;
|
||
ataTurmaSelect.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
if (btnAtasReuniao) {
|
||
btnAtasReuniao.addEventListener('click', () => {
|
||
ataModal.style.display = 'flex';
|
||
populateAtaTurmas();
|
||
});
|
||
}
|
||
if (btnCloseAtaModal) btnCloseAtaModal.addEventListener('click', () => ataModal.style.display = 'none');
|
||
if (ataModal) ataModal.addEventListener('click', (e) => { if (e.target === ataModal) ataModal.style.display = 'none'; });
|
||
|
||
if (btnGerarAta) {
|
||
btnGerarAta.addEventListener('click', async () => {
|
||
const titulo = ataTituloInput ? ataTituloInput.value.trim() : '';
|
||
const tipo = ataTipoSelect ? ataTipoSelect.value : '';
|
||
const discussoes = ataDiscussoesInput ? ataDiscussoesInput.value.trim() : '';
|
||
const pauta = ataPautaInput ? ataPautaInput.value.trim() : '';
|
||
const part = ataParticipantesInput ? ataParticipantesInput.value.split(',').map(p => p.trim()) : [];
|
||
const delib = ataDeliberacoesInput ? ataDeliberacoesInput.value.trim() : '';
|
||
const turmaId = ataTurmaSelect ? ataTurmaSelect.value : null;
|
||
|
||
if (!titulo || !discussoes) {
|
||
alert('Por favor, informe o título da reunião e os tópicos discutidos.');
|
||
return;
|
||
}
|
||
|
||
btnGerarAta.disabled = true;
|
||
btnGerarAta.innerHTML = '⏳ Redigindo Ata Oficial com IA...';
|
||
|
||
try {
|
||
const res = await fetch('/api/atas/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
tipo_reuniao: tipo,
|
||
titulo: titulo,
|
||
turma_id: turmaId,
|
||
pauta: pauta,
|
||
participantes: part,
|
||
discussoes: discussoes,
|
||
deliberacoes: delib
|
||
})
|
||
});
|
||
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || 'Falha ao redigir ata');
|
||
|
||
if (ataOutputText) ataOutputText.textContent = data.textoAta || '';
|
||
alert('✅ Ata formal redigida com sucesso!');
|
||
} catch (err) {
|
||
alert('Erro ao gerar ata: ' + err.message);
|
||
} finally {
|
||
btnGerarAta.disabled = false;
|
||
btnGerarAta.innerHTML = '📋 Gerar Ata Oficial Formatada';
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnCopyAta) {
|
||
btnCopyAta.addEventListener('click', () => {
|
||
if (ataOutputText && ataOutputText.textContent) {
|
||
navigator.clipboard.writeText(ataOutputText.textContent);
|
||
alert('Ata copiada para a área de transferência!');
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnPrintAtaPdf) {
|
||
btnPrintAtaPdf.addEventListener('click', () => {
|
||
if (!ataOutputText || !ataOutputText.textContent) return;
|
||
const win = window.open('', '_blank');
|
||
win.document.write(`
|
||
<!DOCTYPE html>
|
||
<html lang="pt-BR">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Ata de Reunião - Pedagog</title>
|
||
<style>
|
||
body { font-family: 'Times New Roman', Times, serif; padding: 45px; color: #000; line-height: 1.8; max-width: 800px; margin: 0 auto; text-align: justify; }
|
||
h1 { text-align: center; font-size: 1.4rem; text-transform: uppercase; margin-bottom: 25px; }
|
||
.btn-p { background: #8b5cf6; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; cursor: pointer; float: right; font-family: sans-serif; }
|
||
@media print { .btn-p { display: none; } body { padding: 0; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<button class="btn-p" onclick="window.print()">🖨️ Imprimir / Salvar PDF</button>
|
||
<h1>ATA DE REUNIÃO ESCOLAR</h1>
|
||
<div>${escapeHtml(ataOutputText.textContent).replace(/\n/g, '<br>')}</div>
|
||
</body>
|
||
</html>
|
||
`);
|
||
win.document.close();
|
||
});
|
||
}
|
||
|
||
if (btnOpenAtasHistory) {
|
||
btnOpenAtasHistory.addEventListener('click', async () => {
|
||
atasHistoryModal.style.display = 'flex';
|
||
if (!atasHistoryList) return;
|
||
atasHistoryList.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando atas...</p>';
|
||
try {
|
||
const res = await fetch('/api/atas/list');
|
||
const data = await res.json();
|
||
if (data.length === 0) {
|
||
atasHistoryList.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Nenhuma ata salva.</p>';
|
||
return;
|
||
}
|
||
atasHistoryList.innerHTML = data.map(a => `
|
||
<div style="background:rgba(255,255,255,0.02); border:1px solid var(--border-light); padding:12px; border-radius:8px; display:flex; justify-content:space-between; align-items:center;">
|
||
<div>
|
||
<strong style="color:#a78bfa; font-size:0.92rem;">${escapeHtml(a.titulo)}</strong>
|
||
<div style="font-size:0.75rem; color:var(--text-secondary);">${escapeHtml(a.tipo_reuniao)} • ${new Date(a.data_reuniao).toLocaleDateString('pt-BR')}</div>
|
||
</div>
|
||
<button onclick="deleteAtaRecord('${a.id}')" style="background:none; border:none; color:#ef4444; cursor:pointer;" title="Excluir">🗑️</button>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) {
|
||
atasHistoryList.innerHTML = `<p style="color:#ef4444;">Erro: ${e.message}</p>`;
|
||
}
|
||
});
|
||
}
|
||
|
||
window.deleteAtaRecord = async (id) => {
|
||
if (confirm('Deseja excluir esta ata?')) {
|
||
await fetch(`/api/atas/${id}`, { method: 'DELETE' });
|
||
if (btnOpenAtasHistory) btnOpenAtasHistory.click();
|
||
}
|
||
};
|
||
|
||
if (btnCloseAtasHistory) btnCloseAtasHistory.addEventListener('click', () => atasHistoryModal.style.display = 'none');
|
||
|
||
// ------------------------------------------------------------------------
|
||
// 5. STICKERS GAMIFICADOS & CARTELA A4 DE IMPRESSÃO
|
||
// ------------------------------------------------------------------------
|
||
const btnStickersConquistas = document.getElementById('btnStickersConquistas');
|
||
const stickerModal = document.getElementById('stickerModal');
|
||
const btnCloseStickerModal = document.getElementById('btnCloseStickerModal');
|
||
const stickerTurmaSelect = document.getElementById('stickerTurmaSelect');
|
||
const stickerAlunoInput = document.getElementById('stickerAlunoInput');
|
||
const stickerCategoriaSelect = document.getElementById('stickerCategoriaSelect');
|
||
const stickerTituloInput = document.getElementById('stickerTituloInput');
|
||
const stickerDescricaoInput = document.getElementById('stickerDescricaoInput');
|
||
const btnEmitirSticker = document.getElementById('btnEmitirSticker');
|
||
const stickersListGrid = document.getElementById('stickersListGrid');
|
||
const btnPrintStickerSheet = document.getElementById('btnPrintStickerSheet');
|
||
|
||
function populateStickerTurmas() {
|
||
if (!stickerTurmaSelect) return;
|
||
stickerTurmaSelect.innerHTML = '<option value="">-- Selecionar Turma --</option>';
|
||
(currentTurmas || []).forEach(t => {
|
||
const opt = document.createElement('option');
|
||
opt.value = t.id;
|
||
opt.textContent = t.nome;
|
||
stickerTurmaSelect.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
async function loadStickersGrid() {
|
||
if (!stickersListGrid) return;
|
||
stickersListGrid.innerHTML = '<p style="text-align:center; color:var(--text-secondary); grid-column:1/-1;">Carregando...</p>';
|
||
try {
|
||
const res = await fetch('/api/stickers/list');
|
||
const data = await res.json();
|
||
if (data.length === 0) {
|
||
stickersListGrid.innerHTML = '<p style="text-align:center; color:var(--text-secondary); grid-column:1/-1;">Nenhum sticker emitido ainda.</p>';
|
||
return;
|
||
}
|
||
stickersListGrid.innerHTML = data.map(s => `
|
||
<div style="background:rgba(255,255,255,0.03); border:2px dashed rgba(245,158,11,0.4); border-radius:10px; padding:10px; text-align:center; display:flex; flex-direction:column; align-items:center; gap:4px; position:relative;">
|
||
<span style="font-size:1.8rem;">${s.icone || '🌟'}</span>
|
||
<strong style="font-size:0.8rem; color:#fbbf24;">${escapeHtml(s.titulo)}</strong>
|
||
<span style="font-size:0.75rem; color:var(--text-primary);">${escapeHtml(s.aluno_nome)}</span>
|
||
${s.qr_code_url ? `<img src="${s.qr_code_url}" style="width:50px; height:50px; margin-top:4px; border-radius:4px;" title="QR Code para a família">` : ''}
|
||
<button onclick="deleteStickerRecord('${s.id}')" style="position:absolute; top:4px; right:4px; background:none; border:none; color:#ef4444; font-size:0.8rem; cursor:pointer;" title="Excluir">✕</button>
|
||
</div>
|
||
`).join('');
|
||
} catch (e) {
|
||
stickersListGrid.innerHTML = `<p style="color:#ef4444; grid-column:1/-1;">Erro: ${e.message}</p>`;
|
||
}
|
||
}
|
||
|
||
if (btnStickersConquistas) {
|
||
btnStickersConquistas.addEventListener('click', () => {
|
||
stickerModal.style.display = 'flex';
|
||
populateStickerTurmas();
|
||
loadStickersGrid();
|
||
});
|
||
}
|
||
if (btnCloseStickerModal) btnCloseStickerModal.addEventListener('click', () => stickerModal.style.display = 'none');
|
||
if (stickerModal) stickerModal.addEventListener('click', (e) => { if (e.target === stickerModal) stickerModal.style.display = 'none'; });
|
||
|
||
window.deleteStickerRecord = async (id) => {
|
||
if (confirm('Deseja excluir este sticker?')) {
|
||
await fetch(`/api/stickers/${id}`, { method: 'DELETE' });
|
||
loadStickersGrid();
|
||
}
|
||
};
|
||
|
||
if (btnEmitirSticker) {
|
||
btnEmitirSticker.addEventListener('click', async () => {
|
||
const alunoNome = stickerAlunoInput ? stickerAlunoInput.value.trim() : '';
|
||
const titulo = stickerTituloInput ? stickerTituloInput.value.trim() : '';
|
||
const cat = stickerCategoriaSelect ? stickerCategoriaSelect.value : 'Cooperação & Gentileza';
|
||
const desc = stickerDescricaoInput ? stickerDescricaoInput.value.trim() : '';
|
||
const turmaId = stickerTurmaSelect ? stickerTurmaSelect.value : null;
|
||
|
||
if (!alunoNome || !titulo || !desc) {
|
||
alert('Por favor, informe a criança, o título da conquista e a descrição.');
|
||
return;
|
||
}
|
||
|
||
let icone = '🌟';
|
||
if (cat.includes('Cooperação')) icone = '🤝';
|
||
else if (cat.includes('Curiosidade')) icone = '🔬';
|
||
else if (cat.includes('Autonomia')) icone = '🌱';
|
||
else if (cat.includes('Artística')) icone = '🎨';
|
||
else if (cat.includes('Coragem')) icone = '⭐';
|
||
else if (cat.includes('Roda')) icone = '🗣️';
|
||
|
||
try {
|
||
const res = await fetch('/api/stickers/create', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
aluno_nome: alunoNome,
|
||
turma_id: turmaId,
|
||
titulo: titulo,
|
||
categoria: cat,
|
||
icone: icone,
|
||
descricao: desc
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || 'Falha ao emitir sticker');
|
||
alert('✅ Conquista registrada com sucesso!');
|
||
loadStickersGrid();
|
||
} catch (err) {
|
||
alert('Erro: ' + err.message);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Imprimir Cartela A4 com Stickers Recortáveis
|
||
if (btnPrintStickerSheet) {
|
||
btnPrintStickerSheet.addEventListener('click', async () => {
|
||
try {
|
||
const res = await fetch('/api/stickers/list');
|
||
const stickers = await res.json();
|
||
if (stickers.length === 0) {
|
||
alert('Emita ao menos um sticker antes de imprimir a cartela.');
|
||
return;
|
||
}
|
||
|
||
const win = window.open('', '_blank');
|
||
let stickersHtml = '';
|
||
stickers.forEach(s => {
|
||
stickersHtml += `
|
||
<div style="border: 2px dashed #f59e0b; border-radius: 12px; padding: 12px; text-align: center; background: #fffbeb; page-break-inside: avoid; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||
<span style="font-size: 2.2rem;">${s.icone || '🌟'}</span>
|
||
<strong style="font-size: 0.95rem; color: #b45309; margin-top: 4px;">${escapeHtml(s.titulo)}</strong>
|
||
<div style="font-size: 0.85rem; font-weight: bold; color: #1e293b;">${escapeHtml(s.aluno_nome)}</div>
|
||
<p style="font-size: 0.72rem; color: #64748b; margin: 4px 0 6px 0;">${escapeHtml(s.descricao)}</p>
|
||
${s.qr_code_url ? `<img src="${s.qr_code_url}" style="width: 60px; height: 60px; border-radius: 4px; border: 1px solid #e2e8f0;">` : ''}
|
||
<div style="font-size: 0.65rem; color: #94a3b8; margin-top: 4px;">Pedagog • Conquista Escolar</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
win.document.write(`
|
||
<!DOCTYPE html>
|
||
<html lang="pt-BR">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Cartela de Adesivos - Pedagog</title>
|
||
<style>
|
||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; padding: 25px; margin: 0; }
|
||
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; }
|
||
.btn-p { background: #f59e0b; color: white; border: none; padding: 10px 20px; border-radius: 6px; font-weight: bold; cursor: pointer; float: right; margin-bottom: 15px; }
|
||
@media print { .btn-p { display: none; } body { padding: 0; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<button class="btn-p" onclick="window.print()">🖨️ Imprimir em Papel Adesivo / A4</button>
|
||
<h2 style="margin: 0 0 15px 0; color: #b45309;">🌟 Cartela de Conquistas da Turma</h2>
|
||
<div class="grid">
|
||
${stickersHtml}
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`);
|
||
win.document.close();
|
||
} catch (e) {
|
||
alert('Erro ao imprimir cartela: ' + e.message);
|
||
}
|
||
});
|
||
}
|
||
|
||
});
|
||
|