11726 lines
486 KiB
JavaScript
11726 lines
486 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, "'");
|
||
}
|
||
|
||
// Helper global para extração segura de erros de resposta HTTP no frontend
|
||
async function safeExtractError(resp, defaultMsg = 'Erro na operação') {
|
||
if (!resp) return defaultMsg;
|
||
try {
|
||
const text = await resp.text();
|
||
try {
|
||
const json = JSON.parse(text);
|
||
return json.error || defaultMsg;
|
||
} catch (e) {
|
||
if (resp.status === 502 || text.includes('Bad Gateway')) {
|
||
return 'O servidor está temporariamente ocupado processando a requisição. Por favor, tente novamente em instantes.';
|
||
}
|
||
return text.slice(0, 160) || defaultMsg;
|
||
}
|
||
} catch (e) {
|
||
return defaultMsg;
|
||
}
|
||
}
|
||
|
||
|
||
// Função global para exibir notificações elegantes estilo Toast
|
||
function showToast(message, type = 'info') {
|
||
let container = document.getElementById('toast-container');
|
||
if (!container) {
|
||
container = document.createElement('div');
|
||
container.id = 'toast-container';
|
||
container.style.position = 'fixed';
|
||
container.style.bottom = '24px';
|
||
container.style.right = '24px';
|
||
container.style.zIndex = '99999';
|
||
container.style.display = 'flex';
|
||
container.style.flexDirection = 'column';
|
||
container.style.gap = '10px';
|
||
document.body.appendChild(container);
|
||
}
|
||
|
||
const toast = document.createElement('div');
|
||
toast.className = `custom-toast toast-${type}`;
|
||
toast.style.background = type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6';
|
||
toast.style.color = '#fff';
|
||
toast.style.padding = '12px 24px';
|
||
toast.style.borderRadius = '8px';
|
||
toast.style.boxShadow = '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)';
|
||
toast.style.fontFamily = "'Outfit', sans-serif";
|
||
toast.style.fontSize = '0.9rem';
|
||
toast.style.fontWeight = '500';
|
||
toast.style.minWidth = '250px';
|
||
toast.style.transition = 'all 0.3s ease';
|
||
toast.style.opacity = '0';
|
||
toast.style.transform = 'translateY(20px)';
|
||
toast.style.display = 'flex';
|
||
toast.style.alignItems = 'center';
|
||
|
||
// Icon
|
||
const icon = type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️';
|
||
toast.innerHTML = `<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);
|
||
}
|
||
|
||
// ============================================================
|
||
// 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';
|
||
});
|
||
}
|
||
|
||
// 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 (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';
|
||
});
|
||
}
|
||
|
||
// 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';
|
||
});
|
||
}
|
||
|
||
// 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();
|
||
});
|
||
}
|
||
|
||
// 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) {
|
||
console.warn('Projetos de quadrinhos temporariamente indisponíveis:', resp.status);
|
||
return;
|
||
}
|
||
const projects = await resp.json();
|
||
if (!Array.isArray(projects)) return;
|
||
|
||
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.warn('Aviso ao listar projetos:', err.message);
|
||
}
|
||
};
|
||
|
||
const loadComicsProject = async (id) => {
|
||
if (!id) {
|
||
startNewComicsProject();
|
||
return;
|
||
}
|
||
try {
|
||
const resp = await fetch(`/api/comics/projects/${id}`);
|
||
if (!resp.ok) {
|
||
const errMsg = await safeExtractError(resp, 'Falha ao carregar projeto');
|
||
throw new Error(errMsg);
|
||
}
|
||
const { project, panels } = await resp.json();
|
||
|
||
currentComicsProjectId = project.id;
|
||
currentComicsCharDescEnglish = project.character_description_english || '';
|
||
|
||
const modeContainer = document.getElementById('comicsGenerationModeContainer');
|
||
if (modeContainer) modeContainer.style.display = 'flex';
|
||
|
||
if (comicsProjectTitleInput) comicsProjectTitleInput.value = project.titulo || '';
|
||
comicsTemaInput.value = project.tema || '';
|
||
|
||
if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'block';
|
||
if (btnSaveComicsProject) btnSaveComicsProject.style.display = 'block';
|
||
|
||
// Carregar proporção
|
||
selectedComicsRatio = project.proporcao || '16:9';
|
||
document.querySelectorAll('.btn-comics-ratio').forEach(b => {
|
||
if (b.dataset.ratio === selectedComicsRatio) b.classList.add('active');
|
||
else b.classList.remove('active');
|
||
});
|
||
|
||
// Carregar cenário
|
||
if (comicsCenarioSelect) {
|
||
const standardOptions = ['no parque de diversões colorido', 'na sala de aula da escola infantil', 'em uma floresta encantada cheia de flores', 'no zoológico interagindo com animais', 'num jardim ensolarado de uma casa'];
|
||
if (standardOptions.includes(project.cenario)) {
|
||
comicsCenarioSelect.value = project.cenario;
|
||
if (comicsCenarioCustomInput) comicsCenarioCustomInput.style.display = 'none';
|
||
} else {
|
||
comicsCenarioSelect.value = 'custom';
|
||
if (comicsCenarioCustomInput) {
|
||
comicsCenarioCustomInput.style.display = 'block';
|
||
comicsCenarioCustomInput.value = project.cenario || '';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Carregar quantidade e balões baseados nos painéis retornados
|
||
selectedComicsQty = panels.length || 5;
|
||
document.querySelectorAll('.btn-comics-qty').forEach(b => {
|
||
if (parseInt(b.dataset.qty) === selectedComicsQty) b.classList.add('active');
|
||
else b.classList.remove('active');
|
||
});
|
||
|
||
const hasDialogue = panels.some(p => p.dialogue);
|
||
selectedComicsBubbles = hasDialogue;
|
||
document.querySelectorAll('.btn-comics-bubbles').forEach(b => {
|
||
if ((b.dataset.bubbles === 'true') === selectedComicsBubbles) b.classList.add('active');
|
||
else b.classList.remove('active');
|
||
});
|
||
|
||
// Carregar personagens
|
||
comicsCharListContainer.innerHTML = '';
|
||
if (project.character_description_global) {
|
||
try {
|
||
const chars = JSON.parse(project.character_description_global);
|
||
if (Array.isArray(chars)) {
|
||
chars.forEach(c => {
|
||
addCharacterRow(c.type || '', c.name || '', c.isAnimal || false);
|
||
});
|
||
}
|
||
} catch (e) {
|
||
addCharacterRow("Menino", "Lucas", false);
|
||
addCharacterRow("Menina", "Mariana", false);
|
||
}
|
||
} else {
|
||
addCharacterRow("Menino", "Lucas", false);
|
||
addCharacterRow("Menina", "Mariana", false);
|
||
}
|
||
|
||
// Carregar os painéis
|
||
generatedPanelsData = panels.map(p => ({
|
||
panel_number: p.panel_number,
|
||
imageUrl: p.image_url,
|
||
dialogue: p.dialogue || '',
|
||
image_prompt: p.image_prompt || ''
|
||
}));
|
||
|
||
if (generatedPanelsData.length > 0) {
|
||
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';
|
||
|
||
const textarea = document.createElement('textarea');
|
||
textarea.className = 'comic-caption-input';
|
||
textarea.value = panel.dialogue || '';
|
||
textarea.placeholder = 'Digite a fala ou legenda do quadrinho...';
|
||
textarea.addEventListener('input', (e) => {
|
||
panel.dialogue = e.target.value;
|
||
});
|
||
|
||
caption.appendChild(textarea);
|
||
panelCard.appendChild(caption);
|
||
|
||
comicsGridContainer.appendChild(panelCard);
|
||
});
|
||
|
||
comicsResultArea.style.display = 'flex';
|
||
}
|
||
|
||
comicsVideoResult.style.display = 'none';
|
||
comicsVideoLoader.style.display = 'none';
|
||
btnGenerateComics.disabled = false;
|
||
|
||
} catch (err) {
|
||
console.error(err);
|
||
await showCustomAlert('Erro', 'Erro ao carregar projeto: ' + err.message);
|
||
}
|
||
};
|
||
|
||
const saveComicsProjectFlow = async (forceNew = false) => {
|
||
let titulo = comicsProjectTitleInput ? comicsProjectTitleInput.value.trim() : '';
|
||
let tema = comicsTemaInput ? comicsTemaInput.value.trim() : '';
|
||
|
||
if (!titulo) {
|
||
if (comicsResultTitle && comicsResultTitle.textContent.trim() && comicsResultTitle.textContent.trim() !== 'Resultado da História em Quadrinhos') {
|
||
titulo = comicsResultTitle.textContent.trim();
|
||
} else if (tema) {
|
||
titulo = tema;
|
||
} else {
|
||
titulo = 'Minha História em Quadrinhos';
|
||
}
|
||
if (comicsProjectTitleInput) comicsProjectTitleInput.value = titulo;
|
||
}
|
||
|
||
if (!tema) {
|
||
tema = titulo;
|
||
}
|
||
|
||
const characters = [];
|
||
document.querySelectorAll('.comic-char-row').forEach(row => {
|
||
const typeInput = row.querySelector('.char-type-input');
|
||
const nameInput = row.querySelector('.char-name-input');
|
||
const animalToggle = row.querySelector('.comic-char-animal-toggle');
|
||
|
||
const type = typeInput ? typeInput.value.trim() : '';
|
||
const name = nameInput ? nameInput.value.trim() : '';
|
||
const isAnimal = animalToggle ? animalToggle.classList.contains('active') : false;
|
||
if (type && name) {
|
||
characters.push({ type, name, isAnimal });
|
||
}
|
||
});
|
||
|
||
let cenario = comicsCenarioSelect ? comicsCenarioSelect.value : 'no parque de diversões colorido';
|
||
if (cenario === 'custom' && comicsCenarioCustomInput) {
|
||
cenario = comicsCenarioCustomInput.value.trim();
|
||
}
|
||
|
||
const targetId = forceNew ? null : currentComicsProjectId;
|
||
const activeBtn = forceNew ? btnSaveNewComicsProject : btnSaveComicsProject;
|
||
const originalText = activeBtn ? activeBtn.textContent : '';
|
||
|
||
if (activeBtn) {
|
||
activeBtn.disabled = true;
|
||
activeBtn.textContent = '⏳ Salvando...';
|
||
}
|
||
|
||
const body = {
|
||
id: targetId,
|
||
titulo,
|
||
tema,
|
||
cenario,
|
||
proporcao: selectedComicsRatio,
|
||
character_description_global: JSON.stringify(characters),
|
||
character_description_english: currentComicsCharDescEnglish || '',
|
||
panels: generatedPanelsData.map(p => ({
|
||
panel_number: p.panel_number,
|
||
image_url: p.imageUrl || p.image_url || '',
|
||
image_prompt: p.image_prompt || '',
|
||
dialogue: p.dialogue || ''
|
||
}))
|
||
};
|
||
|
||
try {
|
||
const resp = await fetch('/api/comics/projects', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body)
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
const errMsg = await safeExtractError(resp, 'Erro ao salvar projeto');
|
||
throw new Error(errMsg);
|
||
}
|
||
|
||
const data = await resp.json();
|
||
currentComicsProjectId = data.id;
|
||
await showCustomAlert('Sucesso', 'Projeto salvo com sucesso!');
|
||
|
||
if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'block';
|
||
if (btnSaveComicsProject) btnSaveComicsProject.style.display = 'block';
|
||
await loadComicsProjectsList();
|
||
} catch (err) {
|
||
console.error(err);
|
||
await showCustomAlert('Erro', 'Erro ao salvar projeto: ' + err.message);
|
||
} finally {
|
||
if (activeBtn) {
|
||
activeBtn.disabled = false;
|
||
activeBtn.textContent = originalText;
|
||
}
|
||
}
|
||
};
|
||
|
||
const saveComicsProject = () => saveComicsProjectFlow(false);
|
||
const saveNewComicsProject = () => saveComicsProjectFlow(true);
|
||
|
||
const deleteComicsProject = async () => {
|
||
if (!currentComicsProjectId) return;
|
||
const confirmed = await showCustomConfirm(
|
||
'Confirmar Exclusão',
|
||
'Tem certeza de que deseja excluir este projeto permanentemente?',
|
||
true
|
||
);
|
||
if (!confirmed) return;
|
||
|
||
try {
|
||
const resp = await fetch(`/api/comics/projects/${currentComicsProjectId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
const errMsg = await safeExtractError(resp, 'Falha ao excluir projeto');
|
||
throw new Error(errMsg);
|
||
}
|
||
|
||
await showCustomAlert('Excluído', 'Projeto excluído com sucesso!');
|
||
startNewComicsProject();
|
||
await loadComicsProjectsList();
|
||
} catch (err) {
|
||
console.error(err);
|
||
await showCustomAlert('Erro', 'Erro ao excluir projeto: ' + err.message);
|
||
}
|
||
};
|
||
|
||
// Abrir e fechar modal Fábrica de Quadrinhos
|
||
const openComicsModal = () => {
|
||
fabricaQuadrinhosModal.style.display = 'flex';
|
||
startNewComicsProject();
|
||
loadComicsProjectsList();
|
||
};
|
||
|
||
// Listeners do Gerenciador de Projetos
|
||
if (comicsProjectSelect) {
|
||
comicsProjectSelect.addEventListener('change', (e) => {
|
||
loadComicsProject(e.target.value);
|
||
});
|
||
}
|
||
|
||
if (btnNewComicsProject) {
|
||
btnNewComicsProject.addEventListener('click', async () => {
|
||
const confirmed = await showCustomConfirm(
|
||
'Novo Projeto',
|
||
'Deseja iniciar um novo projeto? Alterações não salvas serão perdidas.'
|
||
);
|
||
if (confirmed) {
|
||
startNewComicsProject();
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnSaveComicsProject) {
|
||
btnSaveComicsProject.addEventListener('click', saveComicsProject);
|
||
}
|
||
|
||
if (btnSaveNewComicsProject) {
|
||
btnSaveNewComicsProject.addEventListener('click', saveNewComicsProject);
|
||
}
|
||
|
||
if (btnDeleteComicsProject) {
|
||
btnDeleteComicsProject.addEventListener('click', deleteComicsProject);
|
||
}
|
||
|
||
if (barBtnComics) {
|
||
barBtnComics.addEventListener('click', openComicsModal);
|
||
}
|
||
|
||
if (btnCloseComicsModal) {
|
||
btnCloseComicsModal.addEventListener('click', async () => {
|
||
if (comicsGenerateLoader && comicsGenerateLoader.style.display === 'flex') {
|
||
const confirmed = await showCustomConfirm(
|
||
'Geração em Andamento',
|
||
'Sua história ainda está sendo gerada. Deseja realmente sair e cancelar a criação?'
|
||
);
|
||
if (!confirmed) return;
|
||
}
|
||
fabricaQuadrinhosModal.style.display = 'none';
|
||
if (comicsVideoPlayer) comicsVideoPlayer.pause();
|
||
if (typeof stopComicsMusicPreview === 'function') stopComicsMusicPreview();
|
||
});
|
||
}
|
||
|
||
// Prevenir fechamento acidental ao clicar fora da janela do modal
|
||
window.addEventListener('click', (e) => {
|
||
if (e.target === fabricaQuadrinhosModal) {
|
||
if (comicsGenerateLoader && comicsGenerateLoader.style.display === 'flex') {
|
||
showCustomAlert('Aviso', 'Sua história em quadrinhos está sendo gerada. Por favor, aguarde a conclusão.');
|
||
}
|
||
// O modal permanece aberto para proteger o trabalho da professora contra cliques acidentais fora da janela
|
||
}
|
||
});
|
||
|
||
// Helper para converter URL de imagem local em base64 e obter dimensões reais sem deformar
|
||
const getBase64ImageDetails = async (imgUrl) => {
|
||
return new Promise((resolve, reject) => {
|
||
const img = new Image();
|
||
img.crossOrigin = 'Anonymous';
|
||
img.onload = () => {
|
||
const width = img.naturalWidth || img.width || 1280;
|
||
const height = img.naturalHeight || img.height || 720;
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = width;
|
||
canvas.height = height;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.drawImage(img, 0, 0);
|
||
resolve({
|
||
dataUrl: canvas.toDataURL('image/jpeg'),
|
||
width: width,
|
||
height: height,
|
||
aspectRatio: width / height
|
||
});
|
||
};
|
||
img.onerror = (e) => reject(new Error('Falha ao carregar imagem para o PDF: ' + imgUrl));
|
||
img.src = imgUrl;
|
||
});
|
||
};
|
||
|
||
const getBase64Image = async (imgUrl) => {
|
||
const details = await getBase64ImageDetails(imgUrl);
|
||
return details.dataUrl;
|
||
};
|
||
|
||
// Helper para execução concorrente controlada
|
||
const mapConcurrent = async (items, limit, fn) => {
|
||
const results = new Array(items.length);
|
||
let currentIdx = 0;
|
||
const worker = async () => {
|
||
while (currentIdx < items.length) {
|
||
const i = currentIdx++;
|
||
results[i] = await fn(items[i], i);
|
||
}
|
||
};
|
||
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
||
await Promise.all(workers);
|
||
return results;
|
||
};
|
||
|
||
// Ação de geração da história e imagens
|
||
if (btnGenerateComics) {
|
||
btnGenerateComics.addEventListener('click', async () => {
|
||
const tema = comicsTemaInput.value.trim();
|
||
if (!tema) {
|
||
await showCustomAlert('Campo Requerido', 'Por favor, digite um tema para a história.');
|
||
return;
|
||
}
|
||
|
||
// Reunir personagens dinamicamente
|
||
const personagens = [];
|
||
document.querySelectorAll('.comic-char-row').forEach(row => {
|
||
const type = row.querySelector('.char-type-input').value.trim();
|
||
const name = row.querySelector('.char-name-input').value.trim();
|
||
if (type && name) {
|
||
personagens.push(`${type} chamado(a) ${name}`);
|
||
}
|
||
});
|
||
|
||
if (personagens.length === 0) {
|
||
await showCustomAlert('Personagens Faltando', 'Adicione e preencha pelo menos um personagem principal.');
|
||
return;
|
||
}
|
||
|
||
// Definir cenário
|
||
let cenario = comicsCenarioSelect.value;
|
||
if (cenario === 'custom') {
|
||
cenario = comicsCenarioCustomInput.value.trim() || 'em um local bonito';
|
||
}
|
||
|
||
btnGenerateComics.disabled = true;
|
||
comicsGenerateLoader.style.display = 'flex';
|
||
comicsResultArea.style.display = 'none';
|
||
comicsVideoResult.style.display = 'none';
|
||
comicsLoaderProgress.style.width = '5%';
|
||
comicsLoaderText.textContent = 'Escrevendo o roteiro e diálogos dos quadrinhos...';
|
||
|
||
try {
|
||
// 1. Gerar Roteiro
|
||
const genMode = currentComicsProjectId ? (document.querySelector('input[name="comicsGenMode"]:checked')?.value || 'new_story') : 'new_story';
|
||
|
||
const requestBody = {
|
||
tema,
|
||
quantidade: selectedComicsQty,
|
||
baloes: selectedComicsBubbles,
|
||
proporcao: selectedComicsRatio,
|
||
personagens,
|
||
cenario
|
||
};
|
||
|
||
if (currentComicsProjectId) {
|
||
requestBody.continuationType = genMode;
|
||
requestBody.character_description = currentComicsCharDescEnglish;
|
||
requestBody.existingPanels = generatedPanelsData.map(p => ({
|
||
panel_number: p.panel_number,
|
||
image_url: p.imageUrl || p.image_url || '',
|
||
image_prompt: p.image_prompt || '',
|
||
dialogue: p.dialogue || ''
|
||
}));
|
||
}
|
||
|
||
const scriptResp = await fetch('/api/comics/generate-script', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
if (!scriptResp.ok) {
|
||
const errMsg = await safeExtractError(scriptResp, 'Erro ao planejar roteiro');
|
||
throw new Error(errMsg);
|
||
}
|
||
|
||
const scriptData = await scriptResp.json();
|
||
comicsLoaderProgress.style.width = '20%';
|
||
|
||
if (scriptData.character_description) {
|
||
currentComicsCharDescEnglish = scriptData.character_description;
|
||
}
|
||
|
||
// 2. Gerar quadrinhos com concorrência balanceada (2 por vez) e tracking em tempo real
|
||
if (genMode !== 'extend') {
|
||
generatedPanelsData = [];
|
||
}
|
||
const totalPanels = scriptData.panels.length;
|
||
let completedCount = 0;
|
||
|
||
const CUTE_COMIC_STYLE_SUFFIX = 'adorable cute 3D animated style, Pixar and modern claymation aesthetic, friendly and innocent character design, soft rounded features, big expressive friendly eyes, smooth textures, vibrant warm comforting color palette, whimsical storytelling, sweet and gentle atmosphere, perfect for toddlers and preschoolers (ages 2 to 6), clean studio lighting, 8k render';
|
||
|
||
const generateSinglePanel = async (panel) => {
|
||
let fullPrompt = panel.image_prompt || '';
|
||
if (!fullPrompt.toLowerCase().includes('claymation') && !fullPrompt.toLowerCase().includes('pixar')) {
|
||
fullPrompt = `${fullPrompt}, ${CUTE_COMIC_STYLE_SUFFIX}`;
|
||
}
|
||
fullPrompt = `${fullPrompt}, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`;
|
||
|
||
let attempts = 0;
|
||
let frameData = null;
|
||
|
||
while (attempts < 3 && !frameData) {
|
||
attempts++;
|
||
try {
|
||
const frameResp = await fetch('/api/comics/generate-frame', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ prompt: fullPrompt, proporcao: selectedComicsRatio })
|
||
});
|
||
|
||
if (frameResp.ok) {
|
||
frameData = await frameResp.json();
|
||
} else {
|
||
console.warn(`[Retry Comics] Tentativa ${attempts} para quadro ${panel.panel_number} respondeu ${frameResp.status}`);
|
||
if (attempts < 3) await new Promise(r => setTimeout(r, 1500 * attempts));
|
||
}
|
||
} catch (netErr) {
|
||
console.warn(`[Retry Comics] Tentativa ${attempts} erro de rede no quadro ${panel.panel_number}:`, netErr.message);
|
||
if (attempts < 3) await new Promise(r => setTimeout(r, 1500 * attempts));
|
||
}
|
||
}
|
||
|
||
completedCount++;
|
||
const progressPercent = 20 + Math.floor((completedCount / totalPanels) * 75);
|
||
if (comicsLoaderProgress) comicsLoaderProgress.style.width = `${progressPercent}%`;
|
||
if (comicsLoaderText) comicsLoaderText.textContent = `🎨 Ilustrando quadrinhos: ${completedCount}/${totalPanels} quadros concluídos...`;
|
||
|
||
// Pequeno respiro entre gerações para respeitar a taxa da API
|
||
await new Promise(r => setTimeout(r, 600));
|
||
|
||
if (frameData && frameData.imageUrl) {
|
||
return {
|
||
panel_number: panel.panel_number,
|
||
imageUrl: frameData.imageUrl,
|
||
dialogue: panel.dialogue || '',
|
||
image_prompt: panel.image_prompt || '',
|
||
needsRegeneration: !!frameData.needsRegeneration
|
||
};
|
||
} else {
|
||
console.warn(`[Comics Resiliencia] Quadro ${panel.panel_number} em espera. Preservando a história.`);
|
||
return {
|
||
panel_number: panel.panel_number,
|
||
imageUrl: '/assets/img/placeholder_comic.png',
|
||
dialogue: panel.dialogue || '',
|
||
image_prompt: panel.image_prompt || '',
|
||
needsRegeneration: true
|
||
};
|
||
}
|
||
};
|
||
|
||
// Executa com 1 quadro por vez sequencial para estabilidade absoluta e sem estourar rate limit
|
||
const newPanels = await mapConcurrent(scriptData.panels, 1, generateSinglePanel);
|
||
|
||
// Ordena por panel_number para garantir a sequência correta
|
||
newPanels.sort((a, b) => a.panel_number - b.panel_number);
|
||
|
||
if (genMode === 'extend') {
|
||
generatedPanelsData = generatedPanelsData.concat(newPanels);
|
||
} else {
|
||
generatedPanelsData = newPanels;
|
||
}
|
||
|
||
// Atualizar cabeçalho e metadados
|
||
comicsResultTitle.textContent = scriptData.title || comicsResultTitle.textContent || 'Fábrica de Quadrinhos Pedagog';
|
||
|
||
// Renderizar Storyboard Interativo
|
||
renderComicsStoryboard();
|
||
|
||
// Se for uma nova história derivada de um projeto antigo, limpamos o ID para virar um novo projeto independente ao salvar
|
||
if (genMode === 'new_story' && currentComicsProjectId) {
|
||
currentComicsProjectId = null;
|
||
if (comicsProjectSelect) comicsProjectSelect.value = '';
|
||
const modeContainer = document.getElementById('comicsGenerationModeContainer');
|
||
if (modeContainer) modeContainer.style.display = 'none';
|
||
if (comicsProjectTitleInput) {
|
||
comicsProjectTitleInput.value = scriptData.title || (comicsProjectTitleInput.value + ' (Derivado)');
|
||
}
|
||
if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'none';
|
||
}
|
||
|
||
comicsGenerateLoader.style.display = 'none';
|
||
comicsResultArea.style.display = 'flex';
|
||
btnGenerateComics.disabled = false;
|
||
|
||
// Salvamento automático imediato pós-geração para garantir que a história NUNCA se perca
|
||
try {
|
||
await saveComicsProjectFlow(false);
|
||
await showCustomAlert('🎉 História Gerada e Salva!', 'Sua história em quadrinhos foi concluída e salva automaticamente em "Meus Projetos"!');
|
||
} catch (autoSaveErr) {
|
||
console.warn('Falha no auto-salvamento pós geração:', autoSaveErr.message);
|
||
}
|
||
|
||
} catch (err) {
|
||
console.error('Erro ao fabricar quadrinhos:', err);
|
||
await showCustomAlert('Erro', 'Erro ao fabricar quadrinhos: ' + err.message);
|
||
comicsGenerateLoader.style.display = 'none';
|
||
btnGenerateComics.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- RENDERIZADOR DO STORYBOARD PROFISSIONAL DOS QUADRINHOS ---
|
||
const renderComicsStoryboard = () => {
|
||
if (!comicsGridContainer) return;
|
||
comicsGridContainer.innerHTML = '';
|
||
|
||
generatedPanelsData.forEach((panel, index) => {
|
||
const panelCard = document.createElement('div');
|
||
panelCard.className = 'comic-panel-card';
|
||
panelCard.dataset.index = index;
|
||
|
||
const imgContainer = document.createElement('div');
|
||
imgContainer.className = `comic-panel-image-container ratio-${selectedComicsRatio.replace(':', '-')}`;
|
||
|
||
const img = document.createElement('img');
|
||
img.className = 'comic-panel-image';
|
||
img.src = panel.imageUrl || panel.image_url;
|
||
img.alt = `Quadro ${panel.panel_number}`;
|
||
img.loading = 'lazy';
|
||
imgContainer.appendChild(img);
|
||
|
||
// Selo de número do quadrinho
|
||
const badge = document.createElement('div');
|
||
badge.className = 'comic-panel-number-badge';
|
||
badge.textContent = panel.panel_number;
|
||
imgContainer.appendChild(badge);
|
||
|
||
// Barra de Ações Rápidas por Quadro (Mobile & Desktop)
|
||
const actionsOverlay = document.createElement('div');
|
||
actionsOverlay.className = 'comic-panel-overlay-actions';
|
||
|
||
// Botão 🔍 Zoom
|
||
const btnZoom = document.createElement('button');
|
||
btnZoom.type = 'button';
|
||
btnZoom.className = 'btn-comic-action';
|
||
btnZoom.title = 'Ampliar imagem';
|
||
btnZoom.innerHTML = '🔍';
|
||
btnZoom.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
openComicsPresentation(index);
|
||
});
|
||
actionsOverlay.appendChild(btnZoom);
|
||
|
||
// Botão 🔄 Regenerar Imagem deste Quadro
|
||
const btnRegen = document.createElement('button');
|
||
btnRegen.type = 'button';
|
||
btnRegen.className = 'btn-comic-action';
|
||
btnRegen.title = 'Regenerar apenas este quadrinho';
|
||
btnRegen.innerHTML = '🔄';
|
||
btnRegen.addEventListener('click', async (e) => {
|
||
e.stopPropagation();
|
||
await regenerateSinglePanel(index, panelCard, img, btnRegen);
|
||
});
|
||
actionsOverlay.appendChild(btnRegen);
|
||
|
||
// Botão ✏️ Ver/Editar Prompt
|
||
const btnPromptToggle = document.createElement('button');
|
||
btnPromptToggle.type = 'button';
|
||
btnPromptToggle.className = 'btn-comic-action';
|
||
btnPromptToggle.title = 'Editar prompt visual';
|
||
btnPromptToggle.innerHTML = '✏️';
|
||
actionsOverlay.appendChild(btnPromptToggle);
|
||
|
||
// Botão 💾 Baixar Imagem Individual
|
||
const btnDownloadImg = document.createElement('button');
|
||
btnDownloadImg.type = 'button';
|
||
btnDownloadImg.className = 'btn-comic-action';
|
||
btnDownloadImg.title = 'Baixar esta imagem PNG';
|
||
btnDownloadImg.innerHTML = '💾';
|
||
btnDownloadImg.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const a = document.createElement('a');
|
||
a.href = panel.imageUrl || panel.image_url;
|
||
a.download = `quadrinho_${panel.panel_number}.png`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
a.remove();
|
||
});
|
||
actionsOverlay.appendChild(btnDownloadImg);
|
||
|
||
imgContainer.appendChild(actionsOverlay);
|
||
panelCard.appendChild(imgContainer);
|
||
|
||
// Área de Legenda / Diálogo
|
||
const caption = document.createElement('div');
|
||
caption.className = 'comic-caption-text';
|
||
|
||
const textarea = document.createElement('textarea');
|
||
textarea.className = 'comic-caption-input';
|
||
textarea.value = panel.dialogue || '';
|
||
textarea.placeholder = 'Digite a fala ou legenda do quadrinho...';
|
||
textarea.style.cssText = 'width: 100%; border: 1px solid var(--border-light); background: var(--bg-secondary); color: var(--text-primary); border-radius: 6px; padding: 8px; font-family: inherit; font-size: 0.85rem; resize: vertical; box-sizing: border-box; min-height: 48px;';
|
||
|
||
textarea.addEventListener('input', (e) => {
|
||
panel.dialogue = e.target.value;
|
||
if (generatedPanelsData[index]) {
|
||
generatedPanelsData[index].dialogue = e.target.value;
|
||
}
|
||
});
|
||
caption.appendChild(textarea);
|
||
|
||
// Mini-Editor de Prompt Visual (Oculto por padrão, ativado no botão ✏️)
|
||
const promptBox = document.createElement('div');
|
||
promptBox.className = 'comic-prompt-details';
|
||
const promptInput = document.createElement('textarea');
|
||
promptInput.style.cssText = 'width: 100%; font-size: 0.72rem; padding: 4px; background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-light); border-radius: 4px; resize: vertical; min-height: 40px; box-sizing: border-box;';
|
||
promptInput.value = panel.image_prompt || '';
|
||
promptInput.addEventListener('input', (e) => {
|
||
panel.image_prompt = e.target.value;
|
||
});
|
||
promptBox.appendChild(document.createTextNode('Prompt Visual: '));
|
||
promptBox.appendChild(promptInput);
|
||
caption.appendChild(promptBox);
|
||
|
||
btnPromptToggle.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
promptBox.style.display = promptBox.style.display === 'block' ? 'none' : 'block';
|
||
});
|
||
|
||
panelCard.appendChild(caption);
|
||
comicsGridContainer.appendChild(panelCard);
|
||
});
|
||
};
|
||
|
||
// Regenerar um único painel inteligente (considera textos/legendas alterados e preserva âncora visual de personagens)
|
||
const regenerateSinglePanel = async (index, panelCard, imgElement, btnRegen) => {
|
||
const panel = generatedPanelsData[index];
|
||
if (!panel) return;
|
||
|
||
try {
|
||
btnRegen.disabled = true;
|
||
btnRegen.textContent = '⏳';
|
||
imgElement.style.opacity = '0.4';
|
||
|
||
// Captura o diálogo/texto ATUAL que está escrito na textarea do quadrinho (caso o usuário tenha editado)
|
||
const textarea = panelCard.querySelector('.comic-caption-input');
|
||
const currentDialogue = textarea ? textarea.value.trim() : (panel.dialogue || '');
|
||
panel.dialogue = currentDialogue;
|
||
|
||
// Captura o prompt visual se o mini-editor tiver sido alterado
|
||
const promptInput = panelCard.querySelector('.comic-prompt-details textarea');
|
||
let promptToUse = promptInput ? promptInput.value.trim() : (panel.image_prompt || '');
|
||
|
||
// Se o diálogo tiver sido preenchido/alterado, re-sincronizamos a cena visual com o novo texto
|
||
if (currentDialogue) {
|
||
try {
|
||
const resyncResp = await fetch('/api/comics/resync-frame-prompt', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
currentDialogue,
|
||
oldPrompt: promptToUse,
|
||
character_description: currentComicsCharDescEnglish,
|
||
cenario: comicsCenarioSelect ? comicsCenarioSelect.value : ''
|
||
})
|
||
});
|
||
if (resyncResp.ok) {
|
||
const resyncData = await resyncResp.json();
|
||
if (resyncData.image_prompt) {
|
||
promptToUse = resyncData.image_prompt;
|
||
panel.image_prompt = promptToUse;
|
||
if (promptInput) promptInput.value = promptToUse;
|
||
}
|
||
}
|
||
} catch (resyncErr) {
|
||
console.warn('Re-sincronização de prompt ao regenerar:', resyncErr.message);
|
||
}
|
||
}
|
||
|
||
const CUTE_COMIC_STYLE_SUFFIX = 'adorable cute 3D animated style, Pixar and modern claymation aesthetic, friendly and innocent character design, soft rounded features, big expressive friendly eyes, smooth textures, vibrant warm comforting color palette, whimsical storytelling, sweet and gentle atmosphere, perfect for toddlers and preschoolers (ages 2 to 6), clean studio lighting, 8k render';
|
||
let fullPrompt = promptToUse || '';
|
||
if (!fullPrompt.toLowerCase().includes('claymation') && !fullPrompt.toLowerCase().includes('pixar')) {
|
||
fullPrompt = `${fullPrompt}, ${CUTE_COMIC_STYLE_SUFFIX}`;
|
||
}
|
||
fullPrompt = `${fullPrompt}, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`;
|
||
|
||
const resp = await fetch('/api/comics/generate-frame', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ prompt: fullPrompt, proporcao: selectedComicsRatio })
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
const errMsg = await safeExtractError(resp, 'Erro na regeneração');
|
||
throw new Error(errMsg);
|
||
}
|
||
|
||
const data = await resp.json();
|
||
panel.imageUrl = data.imageUrl;
|
||
imgElement.src = data.imageUrl + '?v=' + Date.now();
|
||
imgElement.style.opacity = '1';
|
||
|
||
// Auto-salva o projeto atualizado no banco
|
||
saveComicsProjectFlow(false).catch(e => console.warn('Auto-save pós regeneração:', e.message));
|
||
|
||
} catch (err) {
|
||
console.error('Erro ao regenerar painel:', err);
|
||
await showCustomAlert('Erro', 'Não foi possível regenerar o quadrinho: ' + err.message);
|
||
imgElement.style.opacity = '1';
|
||
} finally {
|
||
btnRegen.disabled = false;
|
||
btnRegen.innerHTML = '🔄';
|
||
}
|
||
};
|
||
|
||
// --- EXPORTAR TODAS AS IMAGENS EM ZIP ---
|
||
const btnExportComicsZIP = document.getElementById('btnExportComicsZIP');
|
||
if (btnExportComicsZIP) {
|
||
btnExportComicsZIP.addEventListener('click', async () => {
|
||
if (!generatedPanelsData || generatedPanelsData.length === 0) {
|
||
await showCustomAlert('Aviso', 'Nenhum quadrinho gerado para exportar.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
btnExportComicsZIP.disabled = true;
|
||
btnExportComicsZIP.textContent = '⏳ Criando ZIP...';
|
||
|
||
if (typeof JSZip === 'undefined') {
|
||
throw new Error('Biblioteca JSZip não carregada.');
|
||
}
|
||
|
||
const zip = new JSZip();
|
||
const folder = zip.folder('quadrinhos');
|
||
const title = (comicsResultTitle?.textContent || 'Historia_Quadrinhos').replace(/[^a-zA-Z0-9_-]/g, '_');
|
||
|
||
let roteiroTxt = `HISTÓRIA EM QUADRINHOS: ${comicsResultTitle?.textContent || 'Fábrica de Quadrinhos'}\n`;
|
||
roteiroTxt += `Criado com PedagogIA em ${new Date().toLocaleDateString('pt-BR')}\n\n`;
|
||
|
||
for (let i = 0; i < generatedPanelsData.length; i++) {
|
||
const panel = generatedPanelsData[i];
|
||
const imgUrl = panel.imageUrl || panel.image_url;
|
||
roteiroTxt += `[QUADRO ${panel.panel_number}]\nFala/Legenda: ${panel.dialogue || '(Sem fala)'}\nPrompt Visual: ${panel.image_prompt || ''}\n\n`;
|
||
|
||
try {
|
||
const resp = await fetch(imgUrl);
|
||
const blob = await resp.blob();
|
||
folder.file(`quadrinho_${panel.panel_number}.jpg`, blob);
|
||
} catch (fetchErr) {
|
||
console.warn(`Erro ao incluir imagem ${panel.panel_number} no ZIP:`, fetchErr);
|
||
}
|
||
}
|
||
|
||
folder.file('roteiro.txt', roteiroTxt);
|
||
|
||
const content = await zip.generateAsync({ type: 'blob' });
|
||
const downloadUrl = URL.createObjectURL(content);
|
||
const a = document.createElement('a');
|
||
a.href = downloadUrl;
|
||
a.download = `${title}_pedagog.zip`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
a.remove();
|
||
URL.revokeObjectURL(downloadUrl);
|
||
|
||
} catch (err) {
|
||
console.error('Erro ao gerar ZIP:', err);
|
||
await showCustomAlert('Erro', 'Não foi possível gerar o ZIP: ' + err.message);
|
||
} finally {
|
||
btnExportComicsZIP.disabled = false;
|
||
btnExportComicsZIP.innerHTML = '<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, 45, { align: 'center' });
|
||
|
||
const imgDetails = await getBase64ImageDetails(panel.imageUrl);
|
||
const boxWidth = 515;
|
||
const boxMaxHeight = 350;
|
||
const textHeight = 150;
|
||
|
||
// Cálculo exato mantendo a proporção real da imagem (sem esticar)
|
||
let renderW = boxWidth;
|
||
let renderH = renderW / imgDetails.aspectRatio;
|
||
if (renderH > boxMaxHeight) {
|
||
renderH = boxMaxHeight;
|
||
renderW = renderH * imgDetails.aspectRatio;
|
||
}
|
||
|
||
const imgX = 40 + (boxWidth - renderW) / 2;
|
||
const imgY = 70 + (boxMaxHeight - renderH) / 2;
|
||
const cardY = 70;
|
||
const textY = cardY + boxMaxHeight;
|
||
|
||
// Desenha a imagem centralizada e proporcional
|
||
doc.addImage(imgDetails.dataUrl, 'JPEG', imgX, imgY, renderW, renderH);
|
||
|
||
// Fundo da caixa de texto
|
||
doc.setFillColor(248, 250, 252);
|
||
doc.rect(40, textY, boxWidth, textHeight, 'F');
|
||
|
||
// Moldura unificada ao redor de todo o quadro
|
||
doc.setDrawColor(30, 41, 59);
|
||
doc.setLineWidth(2);
|
||
doc.rect(40, cardY, boxWidth, boxMaxHeight + textHeight, 'D');
|
||
|
||
// Linha divisória entre imagem e texto
|
||
doc.line(40, textY, 555, textY);
|
||
|
||
// Texto legível
|
||
if (panel.dialogue) {
|
||
doc.setFont('helvetica', 'normal');
|
||
doc.setFontSize(15);
|
||
doc.setTextColor(15, 23, 42);
|
||
|
||
const splitText = doc.splitTextToSize(panel.dialogue, boxWidth - 40);
|
||
doc.text(splitText, 60, textY + 35);
|
||
}
|
||
|
||
// Numeração do quadrinho
|
||
doc.setFillColor(254, 240, 138);
|
||
doc.circle(65, 95, 12, 'F');
|
||
doc.setDrawColor(30, 41, 59);
|
||
doc.setLineWidth(1.5);
|
||
doc.circle(65, 95, 12, 'D');
|
||
|
||
doc.setFont('helvetica', 'bold');
|
||
doc.setFontSize(10);
|
||
doc.setTextColor(30, 41, 59);
|
||
doc.text(String(panel.panel_number), 65, 99, { align: 'center' });
|
||
|
||
// Rodapé
|
||
doc.setFont('helvetica', 'italic');
|
||
doc.setFontSize(9);
|
||
doc.setTextColor(148, 163, 184);
|
||
doc.text(`Criado com PedaGog - Quadro ${panel.panel_number} de ${generatedPanelsData.length}`, 297, 815, { align: 'center' });
|
||
}
|
||
|
||
doc.save(`quadrinhos_${title.replace(/\s+/g, '_').toLowerCase()}.pdf`);
|
||
} catch (error) {
|
||
console.error('Erro ao gerar PDF Retrato:', error);
|
||
alert('Erro ao gerar PDF: ' + error.message);
|
||
} finally {
|
||
btnExportComicsPDFPortrait.disabled = false;
|
||
btnExportComicsPDFPortrait.textContent = '📄 PDF (1 por página - Retrato)';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Exportar PDF Paisagem (2 por página)
|
||
if (btnExportComicsPDFLandscape) {
|
||
btnExportComicsPDFLandscape.addEventListener('click', async () => {
|
||
if (generatedPanelsData.length === 0) return;
|
||
btnExportComicsPDFLandscape.disabled = true;
|
||
btnExportComicsPDFLandscape.textContent = '⏳ Montando PDF...';
|
||
|
||
try {
|
||
const { jsPDF } = window.jspdf;
|
||
const doc = new jsPDF('l', 'pt', 'a4');
|
||
const title = comicsResultTitle.textContent || 'História em Quadrinhos';
|
||
|
||
const totalPanels = generatedPanelsData.length;
|
||
const totalPages = Math.ceil(totalPanels / 2);
|
||
|
||
for (let page = 0; page < totalPages; page++) {
|
||
if (page > 0) doc.addPage();
|
||
|
||
// Título centralizado no topo
|
||
doc.setFont('helvetica', 'bold');
|
||
doc.setFontSize(18);
|
||
doc.setTextColor(30, 41, 59);
|
||
doc.text(`${title} - Página ${page + 1} de ${totalPages}`, 421, 45, { align: 'center' });
|
||
|
||
// Renderizar até dois quadrinhos lado a lado
|
||
for (let col = 0; col < 2; col++) {
|
||
const index = page * 2 + col;
|
||
if (index >= totalPanels) break;
|
||
|
||
const panel = generatedPanelsData[index];
|
||
const startX = col === 0 ? 40 : 426;
|
||
const boxWidth = 376;
|
||
const boxMaxHeight = 230;
|
||
const textHeight = 140;
|
||
|
||
const imgDetails = await getBase64ImageDetails(panel.imageUrl);
|
||
|
||
// Cálculo rigoroso da dimensão sem deformar (preserva aspect ratio real)
|
||
let renderW = boxWidth;
|
||
let renderH = renderW / imgDetails.aspectRatio;
|
||
if (renderH > boxMaxHeight) {
|
||
renderH = boxMaxHeight;
|
||
renderW = renderH * imgDetails.aspectRatio;
|
||
}
|
||
|
||
const imgX = startX + (boxWidth - renderW) / 2;
|
||
const imgY = 70 + (boxMaxHeight - renderH) / 2;
|
||
const cardY = 70;
|
||
const textY = cardY + boxMaxHeight;
|
||
|
||
// Imagem do quadrinho centralizada e mantendo proporções exatas
|
||
doc.addImage(imgDetails.dataUrl, 'JPEG', imgX, imgY, renderW, renderH);
|
||
|
||
// Fundo da caixa de texto
|
||
doc.setFillColor(248, 250, 252);
|
||
doc.rect(startX, textY, boxWidth, textHeight, 'F');
|
||
|
||
// Moldura unificada ao redor do quadro (imagem + texto)
|
||
doc.setDrawColor(30, 41, 59);
|
||
doc.setLineWidth(2);
|
||
doc.rect(startX, cardY, boxWidth, boxMaxHeight + textHeight, 'D');
|
||
|
||
// Linha separadora entre imagem e caixa de texto
|
||
doc.line(startX, textY, startX + boxWidth, textY);
|
||
|
||
// Legenda formatada
|
||
if (panel.dialogue) {
|
||
doc.setFont('helvetica', 'normal');
|
||
doc.setFontSize(13);
|
||
doc.setTextColor(15, 23, 42);
|
||
|
||
const splitText = doc.splitTextToSize(panel.dialogue, boxWidth - 30);
|
||
doc.text(splitText, startX + 15, textY + 30);
|
||
}
|
||
|
||
// Numeral do quadro
|
||
doc.setFillColor(254, 240, 138);
|
||
doc.circle(startX + 20, 90, 11, 'F');
|
||
doc.setDrawColor(30, 41, 59);
|
||
doc.setLineWidth(1.5);
|
||
doc.circle(startX + 20, 90, 11, 'D');
|
||
|
||
doc.setFont('helvetica', 'bold');
|
||
doc.setFontSize(9);
|
||
doc.setTextColor(30, 41, 59);
|
||
doc.text(String(panel.panel_number), startX + 20, 93, { align: 'center' });
|
||
}
|
||
}
|
||
|
||
doc.save(`quadrinhos_${title.replace(/\s+/g, '_').toLowerCase()}_paisagem.pdf`);
|
||
} catch (error) {
|
||
console.error('Erro ao gerar PDF Paisagem:', error);
|
||
alert('Erro ao gerar PDF: ' + error.message);
|
||
} finally {
|
||
btnExportComicsPDFLandscape.disabled = false;
|
||
btnExportComicsPDFLandscape.textContent = '📄 PDF (2 por página - Paisagem)';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Compilar Vídeo de Apresentação (FFmpeg com Narração IA)
|
||
if (btnCompileComicsVideo) {
|
||
btnCompileComicsVideo.addEventListener('click', async () => {
|
||
if (generatedPanelsData.length === 0) return;
|
||
|
||
stopComicsMusicPreview();
|
||
btnCompileComicsVideo.disabled = true;
|
||
comicsVideoLoader.style.display = 'flex';
|
||
comicsVideoResult.style.display = 'none';
|
||
|
||
const voice = document.getElementById('comicsVideoVoice')?.value || 'mulher';
|
||
|
||
try {
|
||
const response = await fetch('/api/comics/generate-video', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
frames: generatedPanelsData,
|
||
musicSelection: comicsVideoMusic ? comicsVideoMusic.value : 'alegre',
|
||
frameDuration: comicsVideoDuration ? comicsVideoDuration.value : 5,
|
||
narrationVoice: voice,
|
||
titulo: comicsResultTitle?.textContent || 'História em Quadrinhos'
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const err = await response.json();
|
||
throw new Error(err.error || 'Erro na compilação do vídeo');
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
comicsVideoPlayer.src = data.videoUrl;
|
||
comicsVideoDownloadBtn.href = data.videoUrl;
|
||
|
||
comicsVideoResult.style.display = 'flex';
|
||
comicsVideoLoader.style.display = 'none';
|
||
btnCompileComicsVideo.disabled = false;
|
||
|
||
} catch (err) {
|
||
console.error('Erro ao gerar vídeo dos quadrinhos:', err);
|
||
alert('Erro ao compilar o vídeo: ' + err.message);
|
||
comicsVideoLoader.style.display = 'none';
|
||
btnCompileComicsVideo.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- PREVIEW INTERATIVO DE TRILHAS SONORAS (PLAY/STOP) ---
|
||
let comicsPreviewAudio = null;
|
||
const btnPreviewComicsMusic = document.getElementById('btnPreviewComicsMusic');
|
||
const comicsMusicPreviewIcon = document.getElementById('comicsMusicPreviewIcon');
|
||
const comicsMusicPreviewText = document.getElementById('comicsMusicPreviewText');
|
||
const comicsMusicPlayingTag = document.getElementById('comicsMusicPlayingTag');
|
||
|
||
function stopComicsMusicPreview() {
|
||
if (comicsPreviewAudio) {
|
||
comicsPreviewAudio.pause();
|
||
comicsPreviewAudio.currentTime = 0;
|
||
comicsPreviewAudio = null;
|
||
}
|
||
if (comicsMusicPreviewIcon) comicsMusicPreviewIcon.textContent = '▶️';
|
||
if (comicsMusicPreviewText) comicsMusicPreviewText.textContent = 'Ouvir';
|
||
if (comicsMusicPlayingTag) comicsMusicPlayingTag.style.display = 'none';
|
||
if (btnPreviewComicsMusic) {
|
||
btnPreviewComicsMusic.style.background = 'rgba(219, 39, 119, 0.1)';
|
||
btnPreviewComicsMusic.style.borderColor = 'var(--brand-pink)';
|
||
btnPreviewComicsMusic.style.color = 'var(--brand-pink)';
|
||
}
|
||
}
|
||
|
||
if (btnPreviewComicsMusic && comicsVideoMusic) {
|
||
btnPreviewComicsMusic.addEventListener('click', () => {
|
||
const selectedTrack = comicsVideoMusic.value;
|
||
if (selectedTrack === 'sem_musica') {
|
||
showCustomAlert('Sem Trilha', 'A opção "Sem Música de Fundo" está selecionada.');
|
||
return;
|
||
}
|
||
|
||
if (comicsPreviewAudio && !comicsPreviewAudio.paused) {
|
||
stopComicsMusicPreview();
|
||
return;
|
||
}
|
||
|
||
stopComicsMusicPreview();
|
||
const audioUrl = `/assets/audio/${selectedTrack}.mp3`;
|
||
comicsPreviewAudio = new Audio(audioUrl);
|
||
comicsPreviewAudio.volume = 0.55;
|
||
|
||
comicsPreviewAudio.play().then(() => {
|
||
if (comicsMusicPreviewIcon) comicsMusicPreviewIcon.textContent = '⏹️';
|
||
if (comicsMusicPreviewText) comicsMusicPreviewText.textContent = 'Parar';
|
||
if (comicsMusicPlayingTag) comicsMusicPlayingTag.style.display = 'inline';
|
||
if (btnPreviewComicsMusic) {
|
||
btnPreviewComicsMusic.style.background = 'rgba(16, 185, 129, 0.15)';
|
||
btnPreviewComicsMusic.style.borderColor = '#10b981';
|
||
btnPreviewComicsMusic.style.color = '#10b981';
|
||
}
|
||
}).catch(err => {
|
||
console.warn('Erro ao reproduzir prévia de áudio:', err.message);
|
||
stopComicsMusicPreview();
|
||
showCustomAlert('Áudio', 'Não foi possível carregar a prévia desta trilha.');
|
||
});
|
||
|
||
comicsPreviewAudio.onended = () => {
|
||
stopComicsMusicPreview();
|
||
};
|
||
});
|
||
|
||
comicsVideoMusic.addEventListener('change', () => {
|
||
stopComicsMusicPreview();
|
||
});
|
||
}
|
||
|
||
// ============================================================
|
||
// VIDEOMIND (TRANSCRIÇÃO E ANÁLISE DE VÍDEO PEDAGÓGICO)
|
||
// ============================================================
|
||
const videoMindModal = document.getElementById('videoMindModal');
|
||
const btnCloseVideoMindModal = document.getElementById('btnCloseVideoMindModal');
|
||
const barBtnVideoMind = document.getElementById('barBtnVideoMind');
|
||
|
||
const btnGenerateVideoMind = document.getElementById('btnGenerateVideoMind');
|
||
const videoMindUrlInput = document.getElementById('videoMindUrlInput');
|
||
const videoMindLoader = document.getElementById('videoMindLoader');
|
||
const videoMindLoaderText = document.getElementById('videoMindLoaderText');
|
||
const videoMindResultArea = document.getElementById('videoMindResultArea');
|
||
|
||
const videoMindThumbnail = document.getElementById('videoMindThumbnail');
|
||
const videoMindTitle = document.getElementById('videoMindTitle');
|
||
const videoMindAuthor = document.getElementById('videoMindAuthor');
|
||
const videoMindReportArea = document.getElementById('videoMindReportArea');
|
||
|
||
const btnCopyVideoMindReport = document.getElementById('btnCopyVideoMindReport');
|
||
const btnDownloadVideoMindReport = document.getElementById('btnDownloadVideoMindReport');
|
||
const btnPdfVideoMindReport = document.getElementById('btnPdfVideoMindReport');
|
||
|
||
let selectedVideoMindLines = 20;
|
||
let activeVideoMindReport = null;
|
||
|
||
// Listener para selecionar o tamanho do resumo
|
||
document.querySelectorAll('#videoMindModal .music-option-grid .btn-videomind-lines').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('#videoMindModal .music-option-grid .btn-videomind-lines').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
selectedVideoMindLines = parseInt(btn.dataset.lines) || 20;
|
||
});
|
||
});
|
||
|
||
// Abrir modal do VideoMind
|
||
if (barBtnVideoMind) {
|
||
barBtnVideoMind.addEventListener('click', () => {
|
||
videoMindModal.style.display = 'flex';
|
||
videoMindUrlInput.value = '';
|
||
videoMindResultArea.style.display = 'none';
|
||
videoMindReportArea.innerHTML = '';
|
||
if (videoMindLoader) videoMindLoader.style.display = 'none';
|
||
btnGenerateVideoMind.disabled = false;
|
||
activeVideoMindReport = null;
|
||
});
|
||
}
|
||
|
||
// Fechar modal
|
||
if (btnCloseVideoMindModal) {
|
||
btnCloseVideoMindModal.addEventListener('click', () => {
|
||
videoMindModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
// Ação de analisar vídeo
|
||
if (btnGenerateVideoMind) {
|
||
btnGenerateVideoMind.addEventListener('click', async () => {
|
||
const url = videoMindUrlInput.value.trim();
|
||
const pastedTranscriptInput = document.getElementById('videoMindPastedTranscriptInput');
|
||
const pastedTranscript = pastedTranscriptInput ? pastedTranscriptInput.value.trim() : '';
|
||
|
||
if (!url) {
|
||
alert('Por favor, insira o link de um vídeo do YouTube.');
|
||
return;
|
||
}
|
||
|
||
btnGenerateVideoMind.disabled = true;
|
||
videoMindLoader.style.display = 'flex';
|
||
videoMindResultArea.style.display = 'none';
|
||
videoMindLoaderText.textContent = 'Buscando metadados do vídeo...';
|
||
|
||
let statusMsgIdx = 0;
|
||
const statusMessages = [
|
||
'Buscando legenda automática do vídeo...',
|
||
'Carregando a transcrição completa...',
|
||
'Enviando transcrição para a IA...',
|
||
'Pedagoga IA analisando o conteúdo...',
|
||
'Alinhando temas do vídeo com a BNCC...',
|
||
'Gerando resumo de ' + selectedVideoMindLines + ' linhas...',
|
||
'Criando propostas de atividades pedagógicas...',
|
||
'Finalizando o parecer pedagógico...'
|
||
];
|
||
|
||
const statusInterval = setInterval(() => {
|
||
if (statusMsgIdx < statusMessages.length) {
|
||
videoMindLoaderText.textContent = statusMessages[statusMsgIdx];
|
||
statusMsgIdx++;
|
||
} else {
|
||
videoMindLoaderText.textContent = 'Ajustando formatação da análise...';
|
||
}
|
||
}, 4000);
|
||
|
||
try {
|
||
const response = await fetch('/api/videomind/analyze', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
url,
|
||
lines: selectedVideoMindLines,
|
||
pastedTranscript
|
||
})
|
||
});
|
||
|
||
clearInterval(statusInterval);
|
||
|
||
if (!response.ok) {
|
||
const errData = await response.json();
|
||
throw new Error(errData.error || 'Erro desconhecido');
|
||
}
|
||
|
||
const data = await response.json();
|
||
activeVideoMindReport = data;
|
||
|
||
// Preencher informações do vídeo
|
||
videoMindTitle.textContent = data.metadata.title;
|
||
videoMindAuthor.textContent = 'Canal: ' + data.metadata.author;
|
||
videoMindThumbnail.src = data.metadata.thumbnail;
|
||
|
||
// Renderizar Markdown
|
||
if (window.marked) {
|
||
videoMindReportArea.innerHTML = marked.parse(data.report);
|
||
} else {
|
||
videoMindReportArea.textContent = data.report;
|
||
}
|
||
|
||
videoMindLoader.style.display = 'none';
|
||
videoMindResultArea.style.display = 'flex';
|
||
|
||
} catch (err) {
|
||
clearInterval(statusInterval);
|
||
console.error('Erro no VideoMind:', err);
|
||
alert('Erro ao analisar vídeo: ' + err.message);
|
||
videoMindLoader.style.display = 'none';
|
||
btnGenerateVideoMind.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Copiar Parecer
|
||
if (btnCopyVideoMindReport) {
|
||
btnCopyVideoMindReport.addEventListener('click', () => {
|
||
if (activeVideoMindReport && activeVideoMindReport.report) {
|
||
navigator.clipboard.writeText(activeVideoMindReport.report)
|
||
.then(() => {
|
||
btnCopyVideoMindReport.textContent = '✅ Copiado!';
|
||
setTimeout(() => {
|
||
btnCopyVideoMindReport.textContent = '📋 Copiar Parecer';
|
||
}, 2000);
|
||
})
|
||
.catch(err => {
|
||
console.error('Falha ao copiar:', err);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// Baixar relatório TXT
|
||
if (btnDownloadVideoMindReport) {
|
||
btnDownloadVideoMindReport.addEventListener('click', () => {
|
||
if (activeVideoMindReport && activeVideoMindReport.report) {
|
||
const title = activeVideoMindReport.metadata.title || 'analise_video';
|
||
const blob = new Blob([activeVideoMindReport.report], { type: 'text/plain;charset=utf-8' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `VideoMind_${title.replace(/\s+/g, '_').toLowerCase()}.txt`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Baixar relatório PDF
|
||
if (btnPdfVideoMindReport) {
|
||
btnPdfVideoMindReport.addEventListener('click', () => {
|
||
if (!activeVideoMindReport) return;
|
||
|
||
const printWindow = window.open('', '_blank');
|
||
printWindow.document.write(`
|
||
<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';
|
||
});
|
||
}
|
||
|
||
// 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();
|
||
});
|
||
}
|
||
|
||
// 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';
|
||
});
|
||
}
|
||
|
||
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';
|
||
});
|
||
}
|
||
|
||
// 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();
|
||
});
|
||
}
|
||
|
||
// 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
|
||
})
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errMsg = await safeExtractError(res, 'Erro ao compor música');
|
||
throw new Error(errMsg);
|
||
}
|
||
|
||
const data = await res.json();
|
||
|
||
if (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';
|
||
});
|
||
}
|
||
|
||
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) {
|
||
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');
|
||
|
||
// 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;
|
||
historiaCenaImg.onclick = () => window.open(cena.imageUrl, '_blank');
|
||
}
|
||
const historiaCenaOpenBtn = document.getElementById('historiaCenaOpenBtn');
|
||
const historiaCenaDownloadBtn = document.getElementById('historiaCenaDownloadBtn');
|
||
if (historiaCenaOpenBtn) historiaCenaOpenBtn.href = cena.imageUrl;
|
||
if (historiaCenaDownloadBtn) historiaCenaDownloadBtn.href = 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';
|
||
});
|
||
}
|
||
|
||
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');
|
||
|
||
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');
|
||
|
||
// 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 (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 (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');
|
||
|
||
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);
|
||
}
|
||
});
|
||
}
|
||
|
||
});
|
||
|