8932 lines
355 KiB
JavaScript
8932 lines
355 KiB
JavaScript
// Configuração do Markdown parser personalizado com Highlight.js integrado
|
||
const renderer = new marked.Renderer();
|
||
|
||
// Função auxiliar para escapar caracteres HTML
|
||
function escapeHtml(text) {
|
||
return text
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """)
|
||
.replace(/'/g, "'");
|
||
}
|
||
|
||
// Função global para exibir notificações elegantes estilo Toast
|
||
function showToast(message, type = 'info') {
|
||
let container = document.getElementById('toast-container');
|
||
if (!container) {
|
||
container = document.createElement('div');
|
||
container.id = 'toast-container';
|
||
container.style.position = 'fixed';
|
||
container.style.bottom = '24px';
|
||
container.style.right = '24px';
|
||
container.style.zIndex = '99999';
|
||
container.style.display = 'flex';
|
||
container.style.flexDirection = 'column';
|
||
container.style.gap = '10px';
|
||
document.body.appendChild(container);
|
||
}
|
||
|
||
const toast = document.createElement('div');
|
||
toast.className = `custom-toast toast-${type}`;
|
||
toast.style.background = type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6';
|
||
toast.style.color = '#fff';
|
||
toast.style.padding = '12px 24px';
|
||
toast.style.borderRadius = '8px';
|
||
toast.style.boxShadow = '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)';
|
||
toast.style.fontFamily = "'Outfit', sans-serif";
|
||
toast.style.fontSize = '0.9rem';
|
||
toast.style.fontWeight = '500';
|
||
toast.style.minWidth = '250px';
|
||
toast.style.transition = 'all 0.3s ease';
|
||
toast.style.opacity = '0';
|
||
toast.style.transform = 'translateY(20px)';
|
||
toast.style.display = 'flex';
|
||
toast.style.alignItems = 'center';
|
||
|
||
// Icon
|
||
const icon = type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️';
|
||
toast.innerHTML = `<span style="margin-right: 10px; font-size: 1.1rem;">${icon}</span><span>${message}</span>`;
|
||
|
||
container.appendChild(toast);
|
||
|
||
// Trigger animation
|
||
setTimeout(() => {
|
||
toast.style.opacity = '1';
|
||
toast.style.transform = 'translateY(0)';
|
||
}, 10);
|
||
|
||
// Auto remove
|
||
setTimeout(() => {
|
||
toast.style.opacity = '0';
|
||
toast.style.transform = 'translateY(-20px)';
|
||
setTimeout(() => {
|
||
toast.remove();
|
||
}, 300);
|
||
}, 3500);
|
||
}
|
||
window.showToast = showToast; // Tornar acessível globalmente
|
||
|
||
// Função auxiliar para exibir alerta customizado e elegante (substitui o alert nativo)
|
||
function showCustomAlert(title, message) {
|
||
return new Promise((resolve) => {
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'custom-dialog-overlay';
|
||
|
||
overlay.innerHTML = `
|
||
<div class="custom-dialog-card">
|
||
<div class="custom-dialog-header">
|
||
<span>🔔</span>
|
||
<span>${escapeHtml(title)}</span>
|
||
</div>
|
||
<p class="custom-dialog-body">${escapeHtml(message)}</p>
|
||
<div class="custom-dialog-footer">
|
||
<button class="btn-dialog btn-dialog-confirm">OK</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
const btn = overlay.querySelector('.btn-dialog-confirm');
|
||
btn.addEventListener('click', () => {
|
||
overlay.remove();
|
||
resolve();
|
||
});
|
||
|
||
document.body.appendChild(overlay);
|
||
});
|
||
}
|
||
|
||
// Função auxiliar para exibir diálogo de confirmação elegante (substitui o confirm nativo)
|
||
function showCustomConfirm(title, message, isDanger = false) {
|
||
return new Promise((resolve) => {
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'custom-dialog-overlay';
|
||
|
||
overlay.innerHTML = `
|
||
<div class="custom-dialog-card">
|
||
<div class="custom-dialog-header">
|
||
<span>${isDanger ? '⚠️' : '❓'}</span>
|
||
<span>${escapeHtml(title)}</span>
|
||
</div>
|
||
<p class="custom-dialog-body">${escapeHtml(message)}</p>
|
||
<div class="custom-dialog-footer">
|
||
<button class="btn-dialog btn-dialog-cancel">Cancelar</button>
|
||
<button class="btn-dialog ${isDanger ? 'btn-dialog-danger' : 'btn-dialog-confirm'}">${isDanger ? 'Excluir' : 'Confirmar'}</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
const btnCancel = overlay.querySelector('.btn-dialog-cancel');
|
||
const btnConfirm = overlay.querySelector(isDanger ? '.btn-dialog-danger' : '.btn-dialog-confirm');
|
||
|
||
btnCancel.addEventListener('click', () => {
|
||
overlay.remove();
|
||
resolve(false);
|
||
});
|
||
|
||
btnConfirm.addEventListener('click', () => {
|
||
overlay.remove();
|
||
resolve(true);
|
||
});
|
||
|
||
document.body.appendChild(overlay);
|
||
});
|
||
}
|
||
|
||
// Função auxiliar para gerar nome de arquivo de download formatado como <nome_data_hora>.<extensao>
|
||
function getDownloadFileName(type, ext) {
|
||
const now = new Date();
|
||
const year = now.getFullYear();
|
||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||
const day = String(now.getDate()).padStart(2, '0');
|
||
const hours = String(now.getHours()).padStart(2, '0');
|
||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||
return `${type}_${year}-${month}-${day}_${hours}-${minutes}-${seconds}.${ext}`;
|
||
}
|
||
|
||
renderer.code = function(code, language) {
|
||
// Ajuste para lidar com as diferenças na assinatura do marked.js
|
||
const codeContent = typeof code === 'object' ? code.text : code;
|
||
const lang = (typeof code === 'object' ? code.lang : language) || 'txt';
|
||
|
||
return `
|
||
<div class="code-container">
|
||
<div class="code-header">
|
||
<span class="code-lang">${lang}</span>
|
||
<button class="btn-copy-code" onclick="copyCode(this)">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H5.25m11.9-3.664A2.251 2.251 0 0 0 15 2.25h-3a2.251 2.251 0 0 0-2.15 1.586m5.8 0c.065.21.1.433.1.664v.75h-6V4.5c0-.231.035-.454.1-.664M6.75 7.5H4.875c-.621 0-1.125.504-1.125 1.125v12c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V16.5M16.5 9h3.75c.621 0 1.125.504 1.125 1.125v8.25c0 .621-.504 1.125-1.125 1.125H16.5m-3-12h3m-3 3h3M13.5 14.25h3" />
|
||
</svg>
|
||
<span>Copiar</span>
|
||
</button>
|
||
</div>
|
||
<pre><code class="hljs language-${lang}">${escapeHtml(codeContent)}</code></pre>
|
||
</div>
|
||
`;
|
||
};
|
||
|
||
marked.use({ renderer });
|
||
|
||
// Função global de copiar código para que funcione no onclick
|
||
window.copyCode = function(button) {
|
||
const container = button.closest('.code-container');
|
||
const codeElement = container.querySelector('code');
|
||
const textToCopy = codeElement.textContent;
|
||
|
||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||
const textSpan = button.querySelector('span');
|
||
const originalText = textSpan.textContent;
|
||
textSpan.textContent = 'Copiado!';
|
||
button.style.color = 'var(--success)';
|
||
|
||
setTimeout(() => {
|
||
textSpan.textContent = originalText;
|
||
button.style.color = '';
|
||
}, 2000);
|
||
}).catch(err => {
|
||
console.error('Erro ao copiar código:', err);
|
||
});
|
||
};
|
||
|
||
const initApp = () => {
|
||
// Elementos do DOM
|
||
const sidebar = document.getElementById('sidebar');
|
||
const btnMenu = document.getElementById('btnMenu');
|
||
const btnCloseSidebar = document.getElementById('btnCloseSidebar');
|
||
const sidebarOverlay = document.getElementById('sidebarOverlay');
|
||
|
||
const userInput = document.getElementById('userInput');
|
||
const btnSend = document.getElementById('btnSend');
|
||
const btnVoice = document.getElementById('btnVoice');
|
||
const chatForm = document.getElementById('chatForm');
|
||
const messagesContainer = document.getElementById('messagesContainer');
|
||
const welcomeContainer = document.getElementById('welcomeContainer');
|
||
const conversationList = document.getElementById('conversationList');
|
||
|
||
const btnNewChat = document.getElementById('btnNewChat');
|
||
const btnNewChatMobile = document.getElementById('btnNewChatMobile');
|
||
const historyItems = document.getElementById('historyItems');
|
||
const btnLogout = document.getElementById('btnLogout');
|
||
const suggestionCards = document.querySelectorAll('.suggestion-card');
|
||
const searchInput = document.getElementById('searchInput');
|
||
const searchClear = document.getElementById('searchClear');
|
||
const searchResultsHeader = document.getElementById('searchResultsHeader');
|
||
const searchResultsCount = document.getElementById('searchResultsCount');
|
||
const searchClearAll = document.getElementById('searchClearAll');
|
||
const sidebarTags = document.getElementById('sidebarTags');
|
||
|
||
// Configuração de Identidade do Agente (Kemily/Camila)
|
||
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 previewKemilyAvatar = document.getElementById('previewKemilyAvatar');
|
||
const uploadKemilyAvatar = document.getElementById('uploadKemilyAvatar');
|
||
const previewCamilaAvatar = document.getElementById('previewCamilaAvatar');
|
||
const uploadCamilaAvatar = document.getElementById('uploadCamilaAvatar');
|
||
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: "Kemily",
|
||
kemilyAvatarUrl: "assets/kemily.png",
|
||
camilaAvatarUrl: "assets/camila_prof.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}. Como posso ajudar?`;
|
||
}
|
||
if (welcomeAvatarImg) {
|
||
welcomeAvatarImg.src = window.agentConfig.kemilyAvatarUrl;
|
||
}
|
||
if (sidebarAvatarImg) {
|
||
sidebarAvatarImg.src = window.agentConfig.camilaAvatarUrl;
|
||
}
|
||
if (settingAgentName) {
|
||
settingAgentName.value = window.agentConfig.agentName;
|
||
}
|
||
if (previewKemilyAvatar) {
|
||
previewKemilyAvatar.src = window.agentConfig.kemilyAvatarUrl;
|
||
}
|
||
if (previewCamilaAvatar) {
|
||
previewCamilaAvatar.src = window.agentConfig.camilaAvatarUrl;
|
||
}
|
||
// 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 newKemilyAvatarData = null;
|
||
let newCamilaAvatarData = null;
|
||
|
||
// Eventos do Modal
|
||
if (btnSettings) {
|
||
btnSettings.addEventListener('click', () => {
|
||
newKemilyAvatarData = null;
|
||
newCamilaAvatarData = null;
|
||
if (settingAgentName) settingAgentName.value = window.agentConfig.agentName;
|
||
if (previewKemilyAvatar) previewKemilyAvatar.src = window.agentConfig.kemilyAvatarUrl;
|
||
if (previewCamilaAvatar) previewCamilaAvatar.src = window.agentConfig.camilaAvatarUrl;
|
||
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 (uploadKemilyAvatar) {
|
||
uploadKemilyAvatar.addEventListener('change', () => {
|
||
handleAvatarFileSelect(uploadKemilyAvatar, previewKemilyAvatar, (base64) => {
|
||
newKemilyAvatarData = base64;
|
||
});
|
||
});
|
||
}
|
||
|
||
if (uploadCamilaAvatar) {
|
||
uploadCamilaAvatar.addEventListener('change', () => {
|
||
handleAvatarFileSelect(uploadCamilaAvatar, previewCamilaAvatar, (base64) => {
|
||
newCamilaAvatarData = base64;
|
||
});
|
||
});
|
||
}
|
||
|
||
// Salvar Configurações
|
||
if (btnSaveSettings) {
|
||
btnSaveSettings.addEventListener('click', async () => {
|
||
btnSaveSettings.disabled = true;
|
||
btnSaveSettings.innerText = 'Salvando...';
|
||
|
||
try {
|
||
let kemilyUrl = window.agentConfig.kemilyAvatarUrl;
|
||
let camilaUrl = window.agentConfig.camilaAvatarUrl;
|
||
|
||
// Se houver novas imagens, faz upload
|
||
if (newKemilyAvatarData) {
|
||
const res = await fetch('/api/upload-avatar', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ avatarType: 'kemily', base64Data: newKemilyAvatarData })
|
||
});
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
kemilyUrl = data.url;
|
||
}
|
||
}
|
||
|
||
if (newCamilaAvatarData) {
|
||
const res = await fetch('/api/upload-avatar', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ avatarType: 'camila', base64Data: newCamilaAvatarData })
|
||
});
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
camilaUrl = 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,
|
||
kemilyAvatarUrl: kemilyUrl,
|
||
camilaAvatarUrl: camilaUrl,
|
||
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('camila_chats')) || [];
|
||
// Migração: adicionar campos tags e pinned a chats antigos
|
||
chats = chats.map(c => ({ ...c, tags: c.tags || [], pinned: c.pinned || false }));
|
||
let currentChatId = localStorage.getItem('camila_current_chat_id') || null;
|
||
let isGenerating = false;
|
||
let searchQuery = '';
|
||
let activeTag = 'all';
|
||
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('camila_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('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('camila_chats', JSON.stringify(chats));
|
||
if (currentChatId) {
|
||
localStorage.setItem('camila_current_chat_id', currentChatId);
|
||
} else {
|
||
localStorage.removeItem('camila_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('camila_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?.camilaAvatarUrl || 'assets/camila_prof.png'}" alt="Camila" class="user-avatar-img"></div>`
|
||
: `<div class="avatar bot-avatar bot-avatar-img-wrapper"><img src="${window.agentConfig?.kemilyAvatarUrl || 'assets/kemily.png'}" alt="${window.agentConfig?.agentName || 'Kemily'}" 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?.kemilyAvatarUrl || 'assets/kemily.png'}" alt="${window.agentConfig?.agentName || 'Kemily'}" 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('camila_turma') || '[]';
|
||
const res = await fetch('/api/observacao/analisar', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ transcribedText: text, turmaContext: JSON.parse(turmaData) })
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errData = await res.json();
|
||
throw new Error(errData.error || 'Erro na análise');
|
||
}
|
||
|
||
const data = await res.json();
|
||
generatedReport = data.relatorio || data.report;
|
||
generatedCriancas = data.criancas;
|
||
generatedTurma = data.turma || 'não informada';
|
||
generatedTags = data.tags || [];
|
||
showReportPreview(data);
|
||
} catch (err) {
|
||
console.error('Erro ao analisar observação:', err);
|
||
alert('Erro ao analisar observação: ' + err.message);
|
||
showRecordState('transcribed');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Ação de descartar transcrição
|
||
if (btnDiscardTranscription) {
|
||
btnDiscardTranscription.addEventListener('click', () => {
|
||
closeRecordModal();
|
||
});
|
||
}
|
||
|
||
// Mostrar prévia do relatório
|
||
function showReportPreview(data) {
|
||
showRecordState('preview');
|
||
|
||
const now = new Date();
|
||
previewDateTime.textContent = now.toLocaleString('pt-BR', {
|
||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||
hour: '2-digit', minute: '2-digit'
|
||
});
|
||
|
||
// Monta HTML com metadados
|
||
const criancasHtml = data.criancas && data.criancas.length
|
||
? `<div class="report-meta-item"><span class="meta-label">👧 Crianças:</span> ${data.criancas.join(', ')}</div>`
|
||
: `<div class="report-meta-item"><span class="meta-label">👧 Crianças:</span> <em>não identificadas</em></div>`;
|
||
const tagsHtml = data.tags && data.tags.length
|
||
? `<div class="report-meta-item"><span class="meta-label">🏷️ Tags:</span> ${data.tags.map(t => `<span class="tag-chip">${t}</span>`).join(' ')}</div>`
|
||
: '';
|
||
const turmaHtml = data.turma && data.turma !== 'não informada'
|
||
? `<div class="report-meta-item"><span class="meta-label">🏫 Turma:</span> ${data.turma}</div>`
|
||
: '';
|
||
|
||
reportContent.innerHTML = `
|
||
<div class="report-meta">${criancasHtml}${turmaHtml}${tagsHtml}</div>
|
||
<hr style="border-color: var(--border-light); margin: 12px 0;">
|
||
${marked.parse(generatedReport || 'Relatório não disponível.')}
|
||
`;
|
||
reportContent.querySelectorAll('pre code').forEach(b => hljs.highlightElement(b));
|
||
}
|
||
|
||
// Descartar relatório
|
||
if (btnDiscardReport) {
|
||
btnDiscardReport.addEventListener('click', () => {
|
||
generatedReport = null;
|
||
closeRecordModal();
|
||
});
|
||
}
|
||
|
||
// Salvar relatório
|
||
if (btnSaveReport) {
|
||
btnSaveReport.addEventListener('click', async () => {
|
||
if (!generatedReport) return;
|
||
btnSaveReport.disabled = true;
|
||
btnSaveReport.textContent = 'Salvando...';
|
||
|
||
try {
|
||
const res = await fetch('/api/observacao/salvar', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
report: generatedReport,
|
||
criancas: generatedCriancas,
|
||
turma: generatedTurma,
|
||
tags: generatedTags
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (data.success) {
|
||
alert('Relatório salvo com sucesso!');
|
||
} else {
|
||
alert('Erro ao salvar: ' + (data.error || 'desconhecido'));
|
||
}
|
||
} catch (err) {
|
||
console.error('Erro ao salvar:', err);
|
||
alert('Erro ao salvar o relatório.');
|
||
} finally {
|
||
btnSaveReport.disabled = false;
|
||
btnSaveReport.textContent = 'Salvar';
|
||
closeRecordModal();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Sair / descartar
|
||
if (btnExitRecord) {
|
||
btnExitRecord.addEventListener('click', closeRecordModal);
|
||
}
|
||
|
||
// Fechar clicando fora
|
||
if (recordModal) {
|
||
recordModal.addEventListener('click', (e) => {
|
||
if (e.target === recordModal) closeRecordModal();
|
||
});
|
||
}
|
||
|
||
// ============================================================
|
||
// MODAL: MINHAS OBSERVAÇÕES
|
||
// ============================================================
|
||
const obsModal = document.getElementById('obsModal');
|
||
const btnObservacoes = document.getElementById('btnObservacoes');
|
||
const btnObsEspeciais = document.getElementById('btnObsEspeciais');
|
||
const btnCloseObsModal = document.getElementById('btnCloseObsModal');
|
||
const obsModalTitle = document.getElementById('obsModalTitle');
|
||
const obsList = document.getElementById('obsList');
|
||
const obsFilterMes = document.getElementById('obsFilterMes');
|
||
const obsFilterTag = document.getElementById('obsFilterTag');
|
||
const obsSearchCrianca = document.getElementById('obsSearchCrianca');
|
||
|
||
let allObservations = [];
|
||
let isSpecialObsMode = false;
|
||
|
||
// Abrir modal de observações
|
||
if (btnObservacoes) {
|
||
btnObservacoes.addEventListener('click', async () => {
|
||
isSpecialObsMode = false;
|
||
if (obsModalTitle) obsModalTitle.textContent = '📒 Minhas Observações';
|
||
obsModal.style.display = 'flex';
|
||
obsList.innerHTML = '<div class="obs-empty">Carregando...</div>';
|
||
|
||
try {
|
||
const res = await fetch('/api/observacoes');
|
||
const data = await res.json();
|
||
allObservations = data.observations || [];
|
||
populateMonthFilter();
|
||
renderObsList();
|
||
} catch (err) {
|
||
console.error('Erro ao carregar observações:', err);
|
||
obsList.innerHTML = '<div class="obs-empty">Erro ao carregar observações.</div>';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Abrir modal de observações Especiais
|
||
if (btnObsEspeciais) {
|
||
btnObsEspeciais.addEventListener('click', async () => {
|
||
isSpecialObsMode = true;
|
||
if (obsModalTitle) obsModalTitle.textContent = '📒 Observações Educação Especial';
|
||
obsModal.style.display = 'flex';
|
||
obsList.innerHTML = '<div class="obs-empty">Carregando...</div>';
|
||
|
||
try {
|
||
const res = await fetch('/api/observacoes');
|
||
const data = await res.json();
|
||
allObservations = data.observations || [];
|
||
populateMonthFilter();
|
||
renderObsList();
|
||
} catch (err) {
|
||
console.error('Erro ao carregar observações:', err);
|
||
obsList.innerHTML = '<div class="obs-empty">Erro ao carregar observações.</div>';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Fechar modal
|
||
if (btnCloseObsModal) {
|
||
btnCloseObsModal.addEventListener('click', () => {
|
||
obsModal.style.display = 'none';
|
||
});
|
||
}
|
||
if (obsModal) {
|
||
obsModal.addEventListener('click', (e) => {
|
||
if (e.target === obsModal) obsModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
// Filtros
|
||
if (obsFilterMes) obsFilterMes.addEventListener('change', renderObsList);
|
||
if (obsFilterTag) obsFilterTag.addEventListener('change', renderObsList);
|
||
if (obsSearchCrianca) obsSearchCrianca.addEventListener('input', renderObsList);
|
||
|
||
// Preenche select de meses
|
||
function populateMonthFilter() {
|
||
if (!obsFilterMes) return;
|
||
const months = [...new Set(allObservations.map(o => o.date?.slice(0, 7)))]
|
||
.filter(Boolean).sort().reverse();
|
||
obsFilterMes.innerHTML = '<option value="">Todos os meses</option>' +
|
||
months.map(m => `<option value="${m}">${m.split('-')[1]}/${m.split('-')[0]}</option>`).join('');
|
||
}
|
||
|
||
// Renderiza lista filtrada
|
||
function renderObsList() {
|
||
if (!obsList) return;
|
||
const mes = obsFilterMes?.value || '';
|
||
const tag = obsFilterTag?.value || '';
|
||
const search = obsSearchCrianca?.value.toLowerCase() || '';
|
||
|
||
let especialKidsNames = [];
|
||
if (isSpecialObsMode) {
|
||
const turma = JSON.parse(localStorage.getItem('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('camila_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;">
|
||
<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: 8px;">
|
||
<button onclick="editChild('${c.id}')" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 4px;" title="Editar">✏️</button>
|
||
<button onclick="deleteChild('${c.id}')" style="background: none; border: none; color: #ef4444; cursor: pointer; padding: 4px;" title="Excluir">🗑️</button>
|
||
</div>
|
||
</div>
|
||
`}).join('');
|
||
};
|
||
|
||
if(filterAlunosTurma) {
|
||
filterAlunosTurma.addEventListener('change', () => {
|
||
renderAlunosList();
|
||
if(cFormTurma) cFormTurma.value = filterAlunosTurma.value;
|
||
});
|
||
}
|
||
|
||
window.editChild = (id) => {
|
||
const c = currentAlunos.find(x => x.id === id);
|
||
if (!c) return;
|
||
|
||
cFormId.value = c.id;
|
||
cFormTurma.value = c.turma_id || '';
|
||
cFormNome.value = c.nome || '';
|
||
cFormApelido.value = c.apelido || '';
|
||
if(c.data_nasc) {
|
||
cFormDataNasc.value = c.data_nasc.split('T')[0];
|
||
} else {
|
||
cFormDataNasc.value = '';
|
||
}
|
||
cFormEspecial.checked = !!c.especial;
|
||
cFormEspecialDetalhes.value = c.especial_detalhes || '';
|
||
divEspecialDetalhes.style.display = c.especial ? 'flex' : 'none';
|
||
cFormPais.value = c.pais || '';
|
||
cFormAutorizados.value = c.autorizados || '';
|
||
|
||
childFormTitle.textContent = 'Editar Criança';
|
||
};
|
||
|
||
window.deleteChild = async (id) => {
|
||
const isOk = await showCustomConfirm('Excluir', 'Deseja excluir esta criança? Isso não removerá as observações já feitas.', true);
|
||
if (isOk) {
|
||
await fetch('/api/alunos/' + id, { method: 'DELETE' });
|
||
await fetchAlunos();
|
||
renderAlunosList();
|
||
clearChildForm();
|
||
}
|
||
};
|
||
|
||
if (btnMinhaTurma) {
|
||
btnMinhaTurma.addEventListener('click', async () => {
|
||
minhaTurmaModal.style.display = 'flex';
|
||
await fetchTurmas();
|
||
await fetchAlunos();
|
||
renderTurmasList();
|
||
renderAlunosList();
|
||
clearTurmaForm();
|
||
clearChildForm();
|
||
});
|
||
}
|
||
|
||
if (btnCloseMinhaTurmaModal) btnCloseMinhaTurmaModal.addEventListener('click', () => minhaTurmaModal.style.display = 'none');
|
||
if (minhaTurmaModal) {
|
||
minhaTurmaModal.addEventListener('click', (e) => {
|
||
if (e.target === minhaTurmaModal) minhaTurmaModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
if (btnCreateChild) btnCreateChild.addEventListener('click', clearChildForm);
|
||
if (btnCancelChild) btnCancelChild.addEventListener('click', clearChildForm);
|
||
|
||
if (btnSaveChild) {
|
||
btnSaveChild.addEventListener('click', async () => {
|
||
if (!cFormNome.value.trim() || !cFormTurma.value) {
|
||
showCustomAlert('Aviso', 'O Nome da Criança e a Turma são obrigatórios.');
|
||
return;
|
||
}
|
||
|
||
const id = cFormId.value;
|
||
const body = {
|
||
turma_id: cFormTurma.value,
|
||
nome: cFormNome.value.trim(),
|
||
apelido: cFormApelido.value.trim(),
|
||
data_nasc: cFormDataNasc.value || null,
|
||
especial: cFormEspecial.checked,
|
||
especial_detalhes: cFormEspecialDetalhes.value.trim(),
|
||
pais: cFormPais.value.trim(),
|
||
autorizados: cFormAutorizados.value.trim()
|
||
};
|
||
|
||
const url = id ? '/api/alunos/' + id : '/api/alunos';
|
||
const method = id ? 'PUT' : 'POST';
|
||
|
||
try {
|
||
const res = await fetch(url, {
|
||
method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body)
|
||
});
|
||
if(res.ok) {
|
||
await fetchAlunos();
|
||
renderAlunosList();
|
||
clearChildForm();
|
||
showCustomAlert('Sucesso', 'Criança salva com sucesso!');
|
||
} else {
|
||
showCustomAlert('Erro', 'Falha ao salvar criança.');
|
||
}
|
||
} catch(e) { console.error(e); }
|
||
});
|
||
}
|
||
|
||
// Preencher com Voz
|
||
const btnVoiceRecordChild = document.getElementById('btnVoiceRecordChild');
|
||
if (btnVoiceRecordChild && (window.SpeechRecognition || window.webkitSpeechRecognition)) {
|
||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||
const formRecognition = new SpeechRecognition();
|
||
formRecognition.lang = 'pt-BR';
|
||
formRecognition.continuous = false;
|
||
formRecognition.interimResults = false;
|
||
|
||
formRecognition.onstart = () => {
|
||
btnVoiceRecordChild.innerHTML = '🛑 Gravando... (Clique para parar)';
|
||
btnVoiceRecordChild.style.background = 'rgba(239, 68, 68, 0.1)';
|
||
btnVoiceRecordChild.style.color = '#ef4444';
|
||
btnVoiceRecordChild.style.borderColor = '#ef4444';
|
||
};
|
||
|
||
formRecognition.onresult = async (event) => {
|
||
const transcript = event.results[0][0].transcript;
|
||
btnVoiceRecordChild.innerHTML = '⏳ Processando com IA...';
|
||
|
||
try {
|
||
const response = await fetch('/api/chat', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
messages: [{
|
||
role: 'user',
|
||
content: `A professora ditou as seguintes informações para cadastrar uma criança na turma: "${transcript}".
|
||
Por favor, extraia os dados e retorne APENAS um JSON válido com os seguintes campos:
|
||
"nome", "apelido", "data_nasc" (formato YYYY-MM-DD), "especial" (boolean), "especial_detalhes", "pais", "autorizados".
|
||
Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
|
||
}],
|
||
temperature: 0.1
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
const assistantReply = data.message;
|
||
const match = assistantReply.match(/```json\n([\s\S]*?)\n```/) || assistantReply.match(/\{[\s\S]*\}/);
|
||
if (match) {
|
||
const parsed = JSON.parse(match[1] || match[0]);
|
||
if (parsed.nome) cFormNome.value = parsed.nome;
|
||
if (parsed.apelido) cFormApelido.value = parsed.apelido;
|
||
if (parsed.data_nasc) cFormDataNasc.value = parsed.data_nasc;
|
||
if (parsed.especial) cFormEspecial.checked = parsed.especial;
|
||
divEspecialDetalhes.style.display = cFormEspecial.checked ? 'flex' : 'none';
|
||
if (parsed.especial_detalhes) cFormEspecialDetalhes.value = parsed.especial_detalhes;
|
||
if (parsed.pais) cFormPais.value = parsed.pais;
|
||
if (parsed.autorizados) cFormAutorizados.value = parsed.autorizados;
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('Erro ao processar voz:', err);
|
||
showCustomAlert('Erro', 'Não foi possível extrair os dados. Tente novamente ou preencha manualmente.');
|
||
}
|
||
};
|
||
|
||
formRecognition.onend = () => {
|
||
btnVoiceRecordChild.innerHTML = '🎤 Preencher com Voz';
|
||
btnVoiceRecordChild.style.background = 'rgba(16, 163, 127, 0.1)';
|
||
btnVoiceRecordChild.style.color = 'var(--brand-green)';
|
||
btnVoiceRecordChild.style.borderColor = 'var(--brand-green)';
|
||
};
|
||
|
||
btnVoiceRecordChild.addEventListener('click', () => {
|
||
if (btnVoiceRecordChild.innerHTML.includes('Gravando')) {
|
||
formRecognition.stop();
|
||
} else {
|
||
formRecognition.start();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Mostra detalhe da observação em modal
|
||
const showObsDetail = (mdText) => {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'settings-modal';
|
||
modal.style.display = 'flex';
|
||
modal.innerHTML = `
|
||
<div class="settings-modal-content obs-modal-content" style="max-width: 680px;">
|
||
<div class="settings-modal-header">
|
||
<h3>📋 Observação</h3>
|
||
<button class="btn-close-modal" onclick="this.closest('.settings-modal').remove()">
|
||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div style="padding: 20px; overflow-y: auto; max-height: 70vh;">
|
||
<div class="obs-detail-content" style="color: var(--text-primary); line-height: 1.6;">
|
||
${marked.parse(mdText.replace(/^---[\s\S]*?---\n/, ''))}
|
||
</div>
|
||
<div style="margin-top: 16px; display: flex; gap: 8px;">
|
||
<button class="btn-save-report" onclick="navigator.clipboard.writeText(this.closest('.settings-modal').querySelector('.obs-detail-content').innerText).then(()=>alert('Copiado!'))">Copiar Texto</button>
|
||
<button class="btn-discard" onclick="this.closest('.settings-modal').remove()">Fechar</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(modal);
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) modal.remove();
|
||
});
|
||
};
|
||
|
||
// // ============================================================
|
||
// CRUD DE MODELOS DE RELATÓRIO
|
||
// ============================================================
|
||
const modelosRelatorioModal = document.getElementById('modelosRelatorioModal');
|
||
const btnModelosRelatorio = document.getElementById('btnModelosRelatorio');
|
||
const btnCloseModelosModal = document.getElementById('btnCloseModelosModal');
|
||
const templatesList = document.getElementById('templatesList');
|
||
const btnCreateTemplate = document.getElementById('btnCreateTemplate');
|
||
const btnCancelTemplate = document.getElementById('btnCancelTemplate');
|
||
const btnSaveTemplate = document.getElementById('btnSaveTemplate');
|
||
|
||
const templateFormId = document.getElementById('templateFormId');
|
||
const templateFormNome = document.getElementById('templateFormNome');
|
||
const templateFormFinalidade = document.getElementById('templateFormFinalidade');
|
||
const templateFormPeriodicidade = document.getElementById('templateFormPeriodicidade');
|
||
const templateFormEstrutura = document.getElementById('templateFormEstrutura');
|
||
const templateFormTitle = document.getElementById('templateFormTitle');
|
||
|
||
let allTemplates = [];
|
||
|
||
// Abrir modal de modelos
|
||
if (btnModelosRelatorio) {
|
||
btnModelosRelatorio.addEventListener('click', async () => {
|
||
modelosRelatorioModal.style.display = 'flex';
|
||
clearTemplateForm();
|
||
await fetchTemplates();
|
||
});
|
||
}
|
||
|
||
// Fechar modal de modelos
|
||
if (btnCloseModelosModal) {
|
||
btnCloseModelosModal.addEventListener('click', () => {
|
||
modelosRelatorioModal.style.display = 'none';
|
||
});
|
||
}
|
||
if (modelosRelatorioModal) {
|
||
modelosRelatorioModal.addEventListener('click', (e) => {
|
||
if (e.target === modelosRelatorioModal) modelosRelatorioModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
// Buscar modelos do backend
|
||
async function fetchTemplates() {
|
||
templatesList.innerHTML = '<div style="color: var(--text-secondary); text-align: center; padding: 20px;">Carregando...</div>';
|
||
try {
|
||
const res = await fetch('/api/modelos');
|
||
allTemplates = await res.json();
|
||
renderTemplatesList();
|
||
} catch (err) {
|
||
console.error('Erro ao buscar modelos:', err);
|
||
templatesList.innerHTML = '<div style="color: #ff6b6b; text-align: center; padding: 20px;">Erro ao carregar modelos.</div>';
|
||
}
|
||
}
|
||
|
||
// Renderizar a lista de modelos na esquerda
|
||
function renderTemplatesList() {
|
||
if (allTemplates.length === 0) {
|
||
templatesList.innerHTML = '<div style="color: var(--text-secondary); text-align: center; padding: 20px;">Nenhum modelo cadastrado.</div>';
|
||
return;
|
||
}
|
||
templatesList.innerHTML = allTemplates.map(t => `
|
||
<div class="obs-item" style="padding: 10px; margin: 0 0 8px 0; cursor: pointer; border-left: 3px solid var(--brand-green); display: flex; flex-direction: column; gap: 4px; background: var(--bg-secondary); border-radius: 8px;" data-id="${t.id}">
|
||
<div style="font-weight: 600; color: var(--text-primary); font-size: 0.9rem;">${t.nome}</div>
|
||
<div style="font-size: 0.75rem; color: var(--text-secondary);">${t.periodicidade} • ${t.finalidade || 'Sem finalidade'}</div>
|
||
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px;">
|
||
<button class="template-duplicate-btn" data-id="${t.id}" style="background: none; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 2px 8px; border-radius: 4px; font-size: 0.72rem; cursor: pointer; transition: opacity 0.2s;">Duplicar</button>
|
||
<button class="template-edit-btn" data-id="${t.id}" style="background: none; border: 1px solid var(--border-light); color: var(--brand-green); padding: 2px 8px; border-radius: 4px; font-size: 0.72rem; cursor: pointer;">Editar</button>
|
||
<button class="template-delete-btn" data-id="${t.id}" style="background: none; border: 1px solid var(--border-light); color: #ff6b6b; padding: 2px 8px; border-radius: 4px; font-size: 0.72rem; cursor: pointer;">Excluir</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
// Eventos de clique na lista para editar/deletar/duplicar
|
||
templatesList.querySelectorAll('.obs-item').forEach(item => {
|
||
item.addEventListener('click', (e) => {
|
||
if (e.target.tagName === 'BUTTON') return;
|
||
const id = item.dataset.id;
|
||
const template = allTemplates.find(t => t.id == id);
|
||
if (template) fillTemplateForm(template);
|
||
});
|
||
});
|
||
|
||
templatesList.querySelectorAll('.template-duplicate-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const id = btn.dataset.id;
|
||
const template = allTemplates.find(t => t.id == id);
|
||
if (template) {
|
||
fillTemplateForm(template);
|
||
templateFormId.value = '';
|
||
templateFormNome.value = template.nome + ' (Cópia)';
|
||
templateFormTitle.textContent = 'Duplicar Modelo';
|
||
if (templateFormNome) templateFormNome.focus();
|
||
}
|
||
});
|
||
});
|
||
|
||
templatesList.querySelectorAll('.template-edit-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const id = btn.dataset.id;
|
||
const template = allTemplates.find(t => t.id == id);
|
||
if (template) fillTemplateForm(template);
|
||
});
|
||
});
|
||
|
||
templatesList.querySelectorAll('.template-delete-btn').forEach(btn => {
|
||
btn.addEventListener('click', async (e) => {
|
||
e.stopPropagation();
|
||
const id = btn.dataset.id;
|
||
const template = allTemplates.find(t => t.id == id);
|
||
if (!template) return;
|
||
if (confirm(`Tem certeza que deseja excluir o modelo "${template.nome}"?`)) {
|
||
try {
|
||
const res = await fetch(`/api/modelos/${id}`, { method: 'DELETE' });
|
||
if (res.ok) {
|
||
alert('Modelo excluído com sucesso!');
|
||
clearTemplateForm();
|
||
await fetchTemplates();
|
||
} else {
|
||
const err = await res.json();
|
||
alert('Erro ao excluir: ' + err.error);
|
||
}
|
||
} catch (err) {
|
||
console.error('Erro ao excluir modelo:', err);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Preencher formulário de edição
|
||
function fillTemplateForm(template) {
|
||
templateFormId.value = template.id;
|
||
templateFormNome.value = template.nome;
|
||
templateFormFinalidade.value = template.finalidade || '';
|
||
templateFormPeriodicidade.value = template.periodicidade || 'Semanal';
|
||
templateFormEstrutura.value = template.estrutura || '';
|
||
templateFormTitle.textContent = 'Editar Modelo';
|
||
}
|
||
|
||
// Limpar formulário
|
||
function clearTemplateForm() {
|
||
templateFormId.value = '';
|
||
templateFormNome.value = '';
|
||
templateFormFinalidade.value = '';
|
||
templateFormPeriodicidade.value = 'Semanal';
|
||
templateFormEstrutura.value = '';
|
||
templateFormTitle.textContent = 'Novo Modelo';
|
||
}
|
||
|
||
// Botão "Novo Modelo"
|
||
if (btnCreateTemplate) {
|
||
btnCreateTemplate.addEventListener('click', clearTemplateForm);
|
||
}
|
||
|
||
// Botão "Cancelar"
|
||
if (btnCancelTemplate) {
|
||
btnCancelTemplate.addEventListener('click', () => {
|
||
clearTemplateForm();
|
||
if (modelosRelatorioModal) modelosRelatorioModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
// Botão "Salvar Modelo"
|
||
if (btnSaveTemplate) {
|
||
btnSaveTemplate.addEventListener('click', async () => {
|
||
const id = templateFormId.value;
|
||
const nome = templateFormNome.value.trim();
|
||
const finalidade = templateFormFinalidade.value.trim();
|
||
const periodicidade = templateFormPeriodicidade.value;
|
||
const estrutura = templateFormEstrutura.value.trim();
|
||
|
||
if (!nome || !estrutura) {
|
||
alert('Nome e estrutura do modelo são obrigatórios.');
|
||
return;
|
||
}
|
||
|
||
const body = { nome, finalidade, periodicidade, estrutura };
|
||
const url = id ? `/api/modelos/${id}` : '/api/modelos';
|
||
const method = id ? 'PUT' : 'POST';
|
||
|
||
try {
|
||
const res = await fetch(url, {
|
||
method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body)
|
||
});
|
||
if (res.ok) {
|
||
alert('Modelo salvo com sucesso!');
|
||
clearTemplateForm();
|
||
await fetchTemplates();
|
||
} else {
|
||
const err = await res.json();
|
||
alert('Erro ao salvar modelo: ' + err.error);
|
||
}
|
||
} catch (err) {
|
||
console.error('Erro ao salvar modelo:', err);
|
||
}
|
||
});
|
||
}
|
||
|
||
// ============================================================
|
||
// GERAÇÃO / EMISSÃO DE RELATÓRIO PEDAGÓGICO
|
||
// ============================================================
|
||
const emitirRelatorioModal = document.getElementById('emitirRelatorioModal');
|
||
const btnEmitirRelatorio = document.getElementById('btnEmitirRelatorio');
|
||
const btnCloseEmitirModal = document.getElementById('btnCloseEmitirModal');
|
||
|
||
const emitirFilterAno = document.getElementById('emitirFilterAno');
|
||
const emitirFilterTurma = document.getElementById('emitirFilterTurma');
|
||
const emitirFilterCrianca = document.getElementById('emitirFilterCrianca');
|
||
const emitirFilterDataInicio = document.getElementById('emitirFilterDataInicio');
|
||
const emitirFilterDataFim = document.getElementById('emitirFilterDataFim');
|
||
const emitirTagsContainer = document.getElementById('emitirTagsContainer');
|
||
const emitirFilterTemplate = document.getElementById('emitirFilterTemplate');
|
||
let emitirSelectedTags = [];
|
||
|
||
const emitirObsCountInfo = document.getElementById('emitirObsCountInfo');
|
||
const btnGenerateCompiledReport = document.getElementById('btnGenerateCompiledReport');
|
||
const emitirReportPreviewArea = document.getElementById('emitirReportPreviewArea');
|
||
|
||
const btnExportDocx = document.getElementById('btnExportDocx');
|
||
const btnExportPdf = document.getElementById('btnExportPdf');
|
||
|
||
let activeCompiledReport = null; // Guarda o texto do relatório compilado ativo
|
||
|
||
// Abrir modal de emissão
|
||
if (btnEmitirRelatorio) {
|
||
btnEmitirRelatorio.addEventListener('click', async () => {
|
||
emitirRelatorioModal.style.display = 'flex';
|
||
activeCompiledReport = null;
|
||
emitirReportPreviewArea.innerHTML = `
|
||
<div style="color: var(--text-secondary); text-align: center; margin-top: 50px;">
|
||
Escolha uma criança e um modelo de relatório para começar.
|
||
</div>
|
||
`;
|
||
btnExportDocx.disabled = true;
|
||
btnExportDocx.style.cursor = 'not-allowed';
|
||
btnExportDocx.style.borderColor = 'var(--border-light)';
|
||
btnExportDocx.style.color = 'var(--text-secondary)';
|
||
|
||
btnExportPdf.disabled = true;
|
||
btnExportPdf.style.cursor = 'not-allowed';
|
||
btnExportPdf.style.borderColor = 'var(--border-light)';
|
||
btnExportPdf.style.color = 'var(--text-secondary)';
|
||
|
||
emitirFilterDataInicio.value = '';
|
||
emitirFilterDataFim.value = '';
|
||
|
||
emitirSelectedTags = [];
|
||
if (emitirTagsContainer) {
|
||
emitirTagsContainer.querySelectorAll('.tag-pill').forEach(p => p.classList.remove('active'));
|
||
}
|
||
|
||
await loadFiltersData();
|
||
});
|
||
}
|
||
|
||
// Fechar modal de emissão
|
||
if (btnCloseEmitirModal) {
|
||
btnCloseEmitirModal.addEventListener('click', () => {
|
||
emitirRelatorioModal.style.display = 'none';
|
||
});
|
||
}
|
||
if (emitirRelatorioModal) {
|
||
emitirRelatorioModal.addEventListener('click', (e) => {
|
||
if (e.target === emitirRelatorioModal) emitirRelatorioModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
// Buscar crianças com base nos filtros de Ano e Turma selecionados
|
||
async function loadChildrenList() {
|
||
try {
|
||
const activeAno = emitirFilterAno ? emitirFilterAno.value : '';
|
||
const activeTurma = emitirFilterTurma ? emitirFilterTurma.value : '';
|
||
const activeCrianca = emitirFilterCrianca ? emitirFilterCrianca.value : '';
|
||
|
||
let url = '/api/observacoes/criancas';
|
||
const params = [];
|
||
if (activeAno) params.push(`ano=${activeAno}`);
|
||
if (activeTurma) params.push(`turma=${encodeURIComponent(activeTurma)}`);
|
||
if (params.length > 0) url += '?' + params.join('&');
|
||
|
||
const res = await fetch(url);
|
||
const criancas = await res.json();
|
||
emitirFilterCrianca.innerHTML = '<option value="">Selecione a criança</option>' +
|
||
`<option value="todas" ${activeCrianca === 'todas' ? 'selected' : ''}>Todas as crianças (Relatório Global)</option>` +
|
||
(Array.isArray(criancas) ? criancas : []).map(c => `<option value="${c}" ${c === activeCrianca ? 'selected' : ''}>${c}</option>`).join('');
|
||
} catch (err) {
|
||
console.error('Erro ao carregar lista de crianças:', err);
|
||
}
|
||
}
|
||
|
||
// Carregar dados de filtros (anos, turmas, crianças e templates)
|
||
async function loadFiltersData() {
|
||
try {
|
||
const activeAno = emitirFilterAno ? emitirFilterAno.value : '';
|
||
const activeTurma = emitirFilterTurma ? emitirFilterTurma.value : '';
|
||
|
||
const res = await fetch('/api/observacoes/metadata');
|
||
const meta = await res.json();
|
||
|
||
if (emitirFilterAno) {
|
||
emitirFilterAno.innerHTML = '<option value="">Todos os anos</option>' +
|
||
(meta.anos || []).map(y => `<option value="${y}" ${y === activeAno ? 'selected' : ''}>${y}</option>`).join('');
|
||
}
|
||
if (emitirFilterTurma) {
|
||
emitirFilterTurma.innerHTML = '<option value="">Todas as turmas</option>' +
|
||
(meta.turmas || []).map(t => `<option value="${t}" ${t === activeTurma ? 'selected' : ''}>${t}</option>`).join('');
|
||
}
|
||
} catch (err) {
|
||
console.error('Erro ao buscar metadados de filtros:', err);
|
||
}
|
||
|
||
await loadChildrenList();
|
||
|
||
try {
|
||
const activeTemplate = emitirFilterTemplate ? emitirFilterTemplate.value : '';
|
||
const res = await fetch('/api/modelos');
|
||
const modelos = await res.json();
|
||
emitirFilterTemplate.innerHTML = '<option value="">Selecione o modelo</option>' +
|
||
(Array.isArray(modelos) ? modelos : []).map(m => `<option value="${m.id}" ${m.id == activeTemplate ? 'selected' : ''}>${m.nome}</option>`).join('');
|
||
} catch (err) {
|
||
console.error('Erro ao buscar modelos para filtros:', err);
|
||
}
|
||
|
||
updateObservationsCount();
|
||
}
|
||
|
||
// Ouvintes para atualizar contagem e filtros de criança
|
||
if (emitirFilterAno) {
|
||
emitirFilterAno.addEventListener('change', async () => {
|
||
await loadChildrenList();
|
||
updateObservationsCount();
|
||
});
|
||
}
|
||
if (emitirFilterTurma) {
|
||
emitirFilterTurma.addEventListener('change', async () => {
|
||
await loadChildrenList();
|
||
updateObservationsCount();
|
||
});
|
||
}
|
||
|
||
[emitirFilterCrianca, emitirFilterDataInicio, emitirFilterDataFim, emitirFilterTemplate].forEach(el => {
|
||
if (el) el.addEventListener('change', updateObservationsCount);
|
||
});
|
||
if (emitirFilterCrianca) {
|
||
emitirFilterCrianca.addEventListener('input', updateObservationsCount);
|
||
}
|
||
|
||
// Adicionar ouvintes para as pills de tags
|
||
if (emitirTagsContainer) {
|
||
emitirTagsContainer.querySelectorAll('.tag-pill').forEach(pill => {
|
||
pill.addEventListener('click', () => {
|
||
const tag = pill.getAttribute('data-tag');
|
||
if (pill.classList.contains('active')) {
|
||
pill.classList.remove('active');
|
||
emitirSelectedTags = emitirSelectedTags.filter(t => t !== tag);
|
||
} else {
|
||
pill.classList.add('active');
|
||
emitirSelectedTags.push(tag);
|
||
}
|
||
updateObservationsCount();
|
||
});
|
||
});
|
||
}
|
||
|
||
// Recalcular quantidade de observações elegíveis
|
||
async function updateObservationsCount() {
|
||
const crianca = emitirFilterCrianca ? emitirFilterCrianca.value : '';
|
||
const dataInicio = emitirFilterDataInicio ? emitirFilterDataInicio.value : '';
|
||
const dataFim = emitirFilterDataFim ? emitirFilterDataFim.value : '';
|
||
const tag = emitirSelectedTags.join(',');
|
||
const modeloId = emitirFilterTemplate ? emitirFilterTemplate.value : '';
|
||
const ano = emitirFilterAno ? emitirFilterAno.value : '';
|
||
const turma = emitirFilterTurma ? emitirFilterTurma.value : '';
|
||
|
||
if (!crianca || !modeloId) {
|
||
emitirObsCountInfo.textContent = 'Escolha uma criança (ou "Todas") e um modelo para prosseguir.';
|
||
btnGenerateCompiledReport.disabled = true;
|
||
btnGenerateCompiledReport.style.opacity = '0.6';
|
||
btnGenerateCompiledReport.style.cursor = 'not-allowed';
|
||
return;
|
||
}
|
||
|
||
try {
|
||
let url = `/api/observacoes/contar?crianca=${encodeURIComponent(crianca)}`;
|
||
if (dataInicio) url += `&dataInicio=${dataInicio}`;
|
||
if (dataFim) url += `&dataFim=${dataFim}`;
|
||
if (tag) url += `&tags=${encodeURIComponent(tag)}`;
|
||
if (ano) url += `&ano=${ano}`;
|
||
if (turma) url += `&turma=${encodeURIComponent(turma)}`;
|
||
|
||
const res = await fetch(url);
|
||
const data = await res.json();
|
||
const count = data.count || 0;
|
||
|
||
if (count === 0) {
|
||
emitirObsCountInfo.innerHTML = `⚠️ <span style="color: #ff6b6b; font-weight: 600;">Nenhuma observação</span> encontrada para esses filtros.`;
|
||
btnGenerateCompiledReport.disabled = true;
|
||
btnGenerateCompiledReport.style.opacity = '0.6';
|
||
btnGenerateCompiledReport.style.cursor = 'not-allowed';
|
||
} else {
|
||
emitirObsCountInfo.innerHTML = `✨ <strong>${count}</strong> observações selecionadas para compilação.`;
|
||
btnGenerateCompiledReport.disabled = false;
|
||
btnGenerateCompiledReport.style.opacity = '1';
|
||
btnGenerateCompiledReport.style.cursor = 'pointer';
|
||
}
|
||
} catch (err) {
|
||
console.error('Erro ao contar observações:', err);
|
||
}
|
||
}
|
||
|
||
// Gerar relatório consolidado com IA
|
||
if (btnGenerateCompiledReport) {
|
||
btnGenerateCompiledReport.addEventListener('click', async () => {
|
||
const crianca = emitirFilterCrianca.value;
|
||
const dataInicio = emitirFilterDataInicio.value;
|
||
const dataFim = emitirFilterDataFim.value;
|
||
const tags = emitirSelectedTags;
|
||
const modeloId = emitirFilterTemplate.value;
|
||
const ano = emitirFilterAno ? emitirFilterAno.value : '';
|
||
const turma = emitirFilterTurma ? emitirFilterTurma.value : '';
|
||
|
||
if (!crianca || !modeloId) return;
|
||
|
||
emitirReportPreviewArea.innerHTML = `
|
||
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 12px; margin-top: 50px;">
|
||
<div style="width: 32px; height: 32px; border: 3px solid var(--border-light); border-top-color: var(--brand-green); border-radius: 50%; animation: spin 1s linear infinite;"></div>
|
||
<div style="color: var(--text-secondary); font-size: 0.9rem; font-weight: 500;">Analisando observações e gerando relatório pedagógico...</div>
|
||
</div>
|
||
`;
|
||
btnGenerateCompiledReport.disabled = true;
|
||
btnGenerateCompiledReport.style.opacity = '0.6';
|
||
|
||
try {
|
||
const res = await fetch('/api/modelos/gerar-relatorio', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
crianca,
|
||
dataInicio,
|
||
dataFim,
|
||
tags: tags.length > 0 ? tags : undefined,
|
||
modeloId,
|
||
ano,
|
||
turma
|
||
})
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errData = await res.json();
|
||
throw new Error(errData.error || 'Erro desconhecido');
|
||
}
|
||
|
||
const data = await res.json();
|
||
activeCompiledReport = data;
|
||
|
||
// Renderizar prévia do relatório em markdown
|
||
emitirReportPreviewArea.innerHTML = `
|
||
<div style="background: var(--bg-primary); padding: 12px; border-radius: 8px; margin-bottom: 16px; border: 1px solid var(--border-light); font-size: 0.85rem;">
|
||
<div><strong>Estudante:</strong> ${data.crianca}</div>
|
||
<div><strong>Período:</strong> ${data.periodo}</div>
|
||
<div><strong>Modelo Aplicado:</strong> ${data.templateNome}</div>
|
||
<div><strong>Quantidade de Observações Consolidadas:</strong> ${data.totalObservacoes}</div>
|
||
</div>
|
||
<div class="report-content-body" style="color: var(--text-primary);">
|
||
${marked.parse(data.report)}
|
||
</div>
|
||
`;
|
||
|
||
// Ativar botões de exportação
|
||
btnExportDocx.disabled = false;
|
||
btnExportDocx.style.cursor = 'pointer';
|
||
btnExportDocx.style.borderColor = 'var(--brand-green)';
|
||
btnExportDocx.style.color = 'var(--brand-green)';
|
||
|
||
btnExportPdf.disabled = false;
|
||
btnExportPdf.style.cursor = 'pointer';
|
||
btnExportPdf.style.borderColor = 'var(--brand-green)';
|
||
btnExportPdf.style.color = 'var(--brand-green)';
|
||
|
||
// Rolar suavemente até o relatório no mobile
|
||
if (window.innerWidth <= 768) {
|
||
const modalBody = emitirRelatorioModal.querySelector('.settings-modal-body');
|
||
if (modalBody) {
|
||
setTimeout(() => {
|
||
modalBody.scrollTo({
|
||
top: modalBody.scrollHeight,
|
||
behavior: 'smooth'
|
||
});
|
||
}, 150);
|
||
}
|
||
}
|
||
|
||
} catch (err) {
|
||
console.error('Erro ao gerar relatório compilado:', err);
|
||
emitirReportPreviewArea.innerHTML = `
|
||
<div style="color: #ff6b6b; text-align: center; margin-top: 50px; font-weight: 500;">
|
||
Erro ao gerar relatório: ${err.message}
|
||
</div>
|
||
`;
|
||
} finally {
|
||
btnGenerateCompiledReport.disabled = false;
|
||
btnGenerateCompiledReport.style.opacity = '1';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Exportar para DOCX (Formato HTML-DOCX)
|
||
if (btnExportDocx) {
|
||
btnExportDocx.addEventListener('click', () => {
|
||
if (!activeCompiledReport) return;
|
||
|
||
const htmlContent = `
|
||
<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:office:word" xmlns="http://www.w3.org/TR/REC-html40">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Relatório Pedagógico - ${activeCompiledReport.crianca}</title>
|
||
<style>
|
||
body { font-family: Arial, sans-serif; line-height: 1.5; color: #333; }
|
||
h1, h2, h3 { color: #1f2937; }
|
||
.header-info { background-color: #f3f4f6; padding: 12px; border-radius: 6px; margin-bottom: 20px; border: 1px solid #e5e7eb; }
|
||
.header-info div { margin-bottom: 4px; font-size: 11pt; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header-info">
|
||
<div><b>Estudante:</b> ${activeCompiledReport.crianca}</div>
|
||
<div><b>Período:</b> ${activeCompiledReport.periodo}</div>
|
||
<div><b>Modelo:</b> ${activeCompiledReport.templateNome}</div>
|
||
<div><b>Quantidade de observações consolidadas:</b> ${activeCompiledReport.totalObservacoes}</div>
|
||
</div>
|
||
<hr/>
|
||
${marked.parse(activeCompiledReport.report)}
|
||
</body>
|
||
</html>
|
||
`;
|
||
|
||
const blob = new Blob(['\ufeff' + htmlContent], { type: 'application/msword' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `relatorio_${activeCompiledReport.crianca.replace(/\s+/g, '_')}_${Date.now()}.doc`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
});
|
||
}
|
||
|
||
// Exportar para PDF (Abre janela de impressão do navegador)
|
||
if (btnExportPdf) {
|
||
btnExportPdf.addEventListener('click', () => {
|
||
if (!activeCompiledReport) return;
|
||
|
||
const printWindow = window.open('', '_blank');
|
||
printWindow.document.write(`
|
||
<html>
|
||
<head>
|
||
<title>Relatório Pedagógico - ${activeCompiledReport.crianca}</title>
|
||
<style>
|
||
body { font-family: 'Outfit', 'Inter', -apple-system, sans-serif; padding: 40px; color: #333; line-height: 1.6; }
|
||
h1, h2, h3 { color: #1e293b; margin-top: 24px; margin-bottom: 12px; }
|
||
h1 { border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; font-size: 22px; }
|
||
h2 { font-size: 18px; border-bottom: 1px solid #f1f5f9; padding-bottom: 6px; }
|
||
.meta-box { background: #f8fafc; padding: 16px; border-radius: 8px; margin-bottom: 24px; font-size: 14px; border: 1px solid #e2e8f0; }
|
||
.meta-item { margin-bottom: 6px; display: flex; }
|
||
.meta-label { font-weight: bold; color: #475569; width: 220px; }
|
||
p { margin-bottom: 14px; text-align: justify; }
|
||
ul, ol { margin-bottom: 14px; padding-left: 20px; }
|
||
li { margin-bottom: 6px; }
|
||
@media print {
|
||
body { padding: 0; }
|
||
button { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>Relatório Pedagógico de Desenvolvimento</h1>
|
||
<div class="meta-box">
|
||
<div class="meta-item"><span class="meta-label">Estudante:</span> <span>${activeCompiledReport.crianca}</span></div>
|
||
<div class="meta-item"><span class="meta-label">Período:</span> <span>${activeCompiledReport.periodo}</span></div>
|
||
<div class="meta-item"><span class="meta-label">Modelo Aplicado:</span> <span>${activeCompiledReport.templateNome}</span></div>
|
||
<div class="meta-item"><span class="meta-label">Quantidade de Observações:</span> <span>${activeCompiledReport.totalObservacoes}</span></div>
|
||
</div>
|
||
<div>${marked.parse(activeCompiledReport.report)}</div>
|
||
<script>
|
||
window.onload = function() {
|
||
window.print();
|
||
setTimeout(() => { window.close(); }, 500);
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|
||
`);
|
||
printWindow.document.close();
|
||
});
|
||
}
|
||
|
||
// ============================================================
|
||
// ESTÚDIO MUSICAL (Criação de música cantada com MiniMax)
|
||
// ============================================================
|
||
const estudioMusicalModal = document.getElementById('estudioMusicalModal');
|
||
const btnCloseEstudioModal = document.getElementById('btnCloseEstudioModal');
|
||
const sidebarBtnEstudio = document.getElementById('sidebarBtnEstudio');
|
||
const barBtnEstudio = document.getElementById('barBtnEstudio');
|
||
const welcomeCardEstudio = document.getElementById('welcomeCardEstudio');
|
||
|
||
const btnGenerateStudioMusic = document.getElementById('btnGenerateStudioMusic');
|
||
const musicTemaInput = document.getElementById('musicTemaInput');
|
||
const musicGenerateLoader = document.getElementById('musicGenerateLoader');
|
||
const musicLoaderText = document.getElementById('musicLoaderText');
|
||
const musicResultArea = document.getElementById('musicResultArea');
|
||
const musicStudioAudioPlayer = document.getElementById('musicStudioAudioPlayer');
|
||
const musicStudioDownloadBtn = document.getElementById('musicStudioDownloadBtn');
|
||
const musicStudioLyricsArea = document.getElementById('musicStudioLyricsArea');
|
||
const btnCopyMusicLyrics = document.getElementById('btnCopyMusicLyrics');
|
||
const btnDownloadMusicLyrics = document.getElementById('btnDownloadMusicLyrics');
|
||
|
||
let selectedVoice = 'mulher';
|
||
let selectedRhythm = 'roda';
|
||
let selectedDuration = '30s';
|
||
|
||
// Toggle classes active para botões de opções de voz do Estúdio Musical
|
||
document.querySelectorAll('#estudioMusicalModal .music-option-grid .btn-music-option').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('#estudioMusicalModal .music-option-grid .btn-music-option').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
selectedVoice = btn.dataset.voice;
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.btn-music-rhythm').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.btn-music-rhythm').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
selectedRhythm = btn.dataset.rhythm;
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.btn-music-duration').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.btn-music-duration').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
selectedDuration = btn.dataset.duration;
|
||
});
|
||
});
|
||
|
||
// Abrir e fechar modal
|
||
const openEstudioModal = () => {
|
||
estudioMusicalModal.style.display = 'flex';
|
||
musicTemaInput.value = '';
|
||
musicResultArea.style.display = 'none';
|
||
musicStudioAudioPlayer.src = '';
|
||
musicStudioLyricsArea.textContent = '';
|
||
if (musicGenerateLoader) musicGenerateLoader.style.display = 'none';
|
||
btnGenerateStudioMusic.disabled = false;
|
||
};
|
||
|
||
if (sidebarBtnEstudio) {
|
||
sidebarBtnEstudio.addEventListener('click', openEstudioModal);
|
||
}
|
||
|
||
if (barBtnEstudio) {
|
||
barBtnEstudio.addEventListener('click', openEstudioModal);
|
||
}
|
||
|
||
if (welcomeCardEstudio) {
|
||
welcomeCardEstudio.addEventListener('click', openEstudioModal);
|
||
}
|
||
|
||
if (btnCloseEstudioModal) {
|
||
btnCloseEstudioModal.addEventListener('click', () => {
|
||
estudioMusicalModal.style.display = 'none';
|
||
musicStudioAudioPlayer.pause();
|
||
});
|
||
}
|
||
|
||
// Fechar modal clicando fora
|
||
window.addEventListener('click', (e) => {
|
||
if (e.target === estudioMusicalModal) {
|
||
estudioMusicalModal.style.display = 'none';
|
||
musicStudioAudioPlayer.pause();
|
||
}
|
||
});
|
||
|
||
// Ação de gerar música no estúdio
|
||
if (btnGenerateStudioMusic) {
|
||
btnGenerateStudioMusic.addEventListener('click', async () => {
|
||
const tema = musicTemaInput.value.trim();
|
||
if (!tema) {
|
||
alert('Por favor, digite um tema para a música.');
|
||
return;
|
||
}
|
||
|
||
btnGenerateStudioMusic.disabled = true;
|
||
musicGenerateLoader.style.display = 'flex';
|
||
musicResultArea.style.display = 'none';
|
||
|
||
const statusInterval = setInterval(() => {
|
||
const statusMessages = [
|
||
'Compondo a letra pedagógica...',
|
||
'Ajustando a métrica e as rimas...',
|
||
'Gerando o arranjo musical...',
|
||
'Adicionando os vocais cantados...',
|
||
'Quase pronto, finalizando o áudio...'
|
||
];
|
||
const randomMsg = statusMessages[Math.floor(Math.random() * statusMessages.length)];
|
||
musicLoaderText.textContent = randomMsg;
|
||
}, 5000);
|
||
|
||
try {
|
||
const response = await fetch('/api/music/generate', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
tema,
|
||
voz: selectedVoice,
|
||
ritmo: selectedRhythm,
|
||
duracao: selectedDuration
|
||
})
|
||
});
|
||
|
||
clearInterval(statusInterval);
|
||
|
||
if (!response.ok) {
|
||
const errData = await response.json();
|
||
throw new Error(errData.error || 'Erro na geração de áudio');
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
// Exibir resultados
|
||
musicStudioLyricsArea.textContent = data.lyrics;
|
||
musicStudioAudioPlayer.src = data.audioUrl;
|
||
musicStudioDownloadBtn.href = data.audioUrl;
|
||
|
||
musicGenerateLoader.style.display = 'none';
|
||
musicResultArea.style.display = 'flex';
|
||
|
||
} catch (err) {
|
||
clearInterval(statusInterval);
|
||
console.error('Erro ao gerar música no estúdio:', err);
|
||
alert('Erro ao gerar música: ' + err.message);
|
||
musicGenerateLoader.style.display = 'none';
|
||
btnGenerateStudioMusic.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Copiar letra
|
||
if (btnCopyMusicLyrics) {
|
||
btnCopyMusicLyrics.addEventListener('click', () => {
|
||
const lyrics = musicStudioLyricsArea.textContent;
|
||
if (lyrics) {
|
||
navigator.clipboard.writeText(lyrics)
|
||
.then(() => {
|
||
btnCopyMusicLyrics.textContent = '✅ Copiado!';
|
||
setTimeout(() => {
|
||
btnCopyMusicLyrics.textContent = '📋 Copiar Letra';
|
||
}, 2000);
|
||
})
|
||
.catch(err => {
|
||
console.error('Falha ao copiar:', err);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// Baixar letra em arquivo de texto (TXT)
|
||
if (btnDownloadMusicLyrics) {
|
||
btnDownloadMusicLyrics.addEventListener('click', () => {
|
||
const lyrics = musicStudioLyricsArea.textContent;
|
||
const tema = musicTemaInput.value.trim() || 'cantiga';
|
||
if (lyrics) {
|
||
const blob = new Blob([lyrics], { type: 'text/plain;charset=utf-8' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `letra_${tema.replace(/\s+/g, '_').toLowerCase()}.txt`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
});
|
||
}
|
||
|
||
// =====================================
|
||
// HISTÓRICO DO ESTÚDIO MUSICAL
|
||
// =====================================
|
||
const btnOpenEstudioHistory = document.getElementById('btnOpenEstudioHistory');
|
||
const estudioHistoryModal = document.getElementById('estudioHistoryModal');
|
||
const btnCloseEstudioHistory = document.getElementById('btnCloseEstudioHistory');
|
||
const estudioHistoryList = document.getElementById('estudioHistoryList');
|
||
|
||
if (btnOpenEstudioHistory) {
|
||
btnOpenEstudioHistory.addEventListener('click', async () => {
|
||
estudioHistoryModal.style.display = 'flex';
|
||
estudioHistoryList.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">Carregando histórico...</div>';
|
||
try {
|
||
const res = await fetch('/api/music/list');
|
||
const data = await res.json();
|
||
estudioHistoryList.innerHTML = '';
|
||
if (data.length === 0) {
|
||
estudioHistoryList.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">Nenhuma canção salva ainda no estúdio.</div>';
|
||
return;
|
||
}
|
||
data.forEach(item => {
|
||
const div = document.createElement('div');
|
||
div.style.background = 'var(--bg-tertiary)';
|
||
div.style.padding = '12px 15px';
|
||
div.style.borderRadius = '8px';
|
||
div.style.display = 'flex';
|
||
div.style.justifyContent = 'space-between';
|
||
div.style.alignItems = 'center';
|
||
div.innerHTML = `
|
||
<div style="flex: 1; margin-right: 15px;">
|
||
<div style="font-weight: 600; font-size: 0.95rem; color: var(--text-primary);">🎵 ${item.tema || 'Música'}</div>
|
||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-top: 4px;">Voz: ${item.voz} | Ritmo: ${item.ritmo}</div>
|
||
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 4px;">${new Date(item.created_at).toLocaleString()}</div>
|
||
</div>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button class="btn-ver-estudio btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.85rem; border-radius: 6px;">Ver/Ouvir</button>
|
||
<button class="btn-del-estudio btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.85rem; border-radius: 6px; color: #ef4444; border-color: rgba(239, 68, 68, 0.3);">🗑️</button>
|
||
</div>
|
||
`;
|
||
estudioHistoryList.appendChild(div);
|
||
});
|
||
|
||
document.querySelectorAll('.btn-ver-estudio').forEach(btn => {
|
||
btn.addEventListener('click', async (e) => {
|
||
const id = e.currentTarget.dataset.id;
|
||
try {
|
||
const res = await fetch(`/api/music/${id}`);
|
||
const itemData = await res.json();
|
||
|
||
musicStudioLyricsArea.textContent = itemData.letra;
|
||
musicStudioAudioPlayer.src = itemData.audio_url;
|
||
musicStudioDownloadBtn.href = itemData.audio_url;
|
||
musicResultArea.style.display = 'flex';
|
||
|
||
estudioHistoryModal.style.display = 'none';
|
||
} catch (err) { alert('Erro ao carregar música do estúdio.'); }
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.btn-del-estudio').forEach(btn => {
|
||
btn.addEventListener('click', async (e) => {
|
||
if(!confirm('Tem certeza que deseja apagar esta canção?')) return;
|
||
const id = e.currentTarget.dataset.id;
|
||
try {
|
||
await fetch(`/api/music/${id}`, { method: 'DELETE' });
|
||
e.currentTarget.closest('div[style*="var(--bg-tertiary)"]').remove();
|
||
} catch (err) { alert('Erro ao deletar.'); }
|
||
});
|
||
});
|
||
|
||
} catch (err) {
|
||
estudioHistoryList.innerHTML = '<div style="color: #ef4444; padding: 20px;">Erro ao carregar histórico.</div>';
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnCloseEstudioHistory) {
|
||
btnCloseEstudioHistory.addEventListener('click', () => {
|
||
estudioHistoryModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
|
||
// ============================================================
|
||
// FÁBRICA DE QUADRINHOS (Criação de histórias visuais pedagógicas)
|
||
// ============================================================
|
||
const fabricaQuadrinhosModal = document.getElementById('fabricaQuadrinhosModal');
|
||
const btnCloseComicsModal = document.getElementById('btnCloseComicsModal');
|
||
const barBtnComics = document.getElementById('barBtnComics');
|
||
|
||
const btnGenerateComics = document.getElementById('btnGenerateComics');
|
||
const comicsTemaInput = document.getElementById('comicsTemaInput');
|
||
const comicsGenerateLoader = document.getElementById('comicsGenerateLoader');
|
||
const comicsLoaderText = document.getElementById('comicsLoaderText');
|
||
const comicsLoaderProgress = document.getElementById('comicsLoaderProgress');
|
||
|
||
const comicsResultArea = document.getElementById('comicsResultArea');
|
||
const comicsResultTitle = document.getElementById('comicsResultTitle');
|
||
const comicsGridContainer = document.getElementById('comicsGridContainer');
|
||
|
||
const comicsCenarioSelect = document.getElementById('comicsCenarioSelect');
|
||
const comicsCenarioCustomInput = document.getElementById('comicsCenarioCustomInput');
|
||
|
||
// Export PDF / Vídeo
|
||
const btnExportComicsPDFPortrait = document.getElementById('btnExportComicsPDFPortrait');
|
||
const btnExportComicsPDFLandscape = document.getElementById('btnExportComicsPDFLandscape');
|
||
const btnCompileComicsVideo = document.getElementById('btnCompileComicsVideo');
|
||
const comicsVideoMusic = document.getElementById('comicsVideoMusic');
|
||
const comicsVideoDuration = document.getElementById('comicsVideoDuration');
|
||
const comicsVideoLoader = document.getElementById('comicsVideoLoader');
|
||
const comicsVideoResult = document.getElementById('comicsVideoResult');
|
||
const comicsVideoPlayer = document.getElementById('comicsVideoPlayer');
|
||
const comicsVideoDownloadBtn = document.getElementById('comicsVideoDownloadBtn');
|
||
|
||
const comicsProjectSelect = document.getElementById('comicsProjectSelect');
|
||
const btnNewComicsProject = document.getElementById('btnNewComicsProject');
|
||
const btnDeleteComicsProject = document.getElementById('btnDeleteComicsProject');
|
||
const comicsProjectTitleInput = document.getElementById('comicsProjectTitleInput');
|
||
const btnSaveComicsProject = document.getElementById('btnSaveComicsProject');
|
||
const btnSaveNewComicsProject = document.getElementById('btnSaveNewComicsProject');
|
||
|
||
let currentComicsProjectId = null;
|
||
let currentComicsCharDescEnglish = '';
|
||
let selectedComicsQty = 5;
|
||
let selectedComicsBubbles = true;
|
||
let selectedComicsRatio = '16:9';
|
||
let generatedPanelsData = [];
|
||
|
||
// Toggle ativo quantidade de quadros
|
||
document.querySelectorAll('.btn-comics-qty').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.btn-comics-qty').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
selectedComicsQty = parseInt(btn.dataset.qty);
|
||
});
|
||
});
|
||
|
||
// Toggle ativo falas/diálogos
|
||
document.querySelectorAll('.btn-comics-bubbles').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.btn-comics-bubbles').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
selectedComicsBubbles = btn.dataset.bubbles === 'true';
|
||
});
|
||
});
|
||
|
||
// Toggle ativo proporção visual
|
||
document.querySelectorAll('.btn-comics-ratio').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.btn-comics-ratio').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
selectedComicsRatio = btn.dataset.ratio;
|
||
});
|
||
});
|
||
|
||
// Gerenciador dinâmico de personagens
|
||
const comicsCharListContainer = document.getElementById('comicsCharListContainer');
|
||
const btnAddHumanChar = document.getElementById('btnAddHumanChar');
|
||
const btnAddAnimalChar = document.getElementById('btnAddAnimalChar');
|
||
|
||
const addCharacterRow = (type = '', name = '', isAnimal = false) => {
|
||
const row = document.createElement('div');
|
||
row.className = 'comic-char-row';
|
||
row.style.cssText = 'display: flex; gap: 8px; align-items: center; background: var(--bg-primary); padding: 8px; border-radius: 8px; border: 1px solid var(--border-light);';
|
||
|
||
const emoji = document.createElement('span');
|
||
emoji.style.fontSize = '1.1rem';
|
||
emoji.textContent = isAnimal ? '🐾' : '🧒';
|
||
row.appendChild(emoji);
|
||
|
||
const typeInput = document.createElement('input');
|
||
typeInput.type = 'text';
|
||
typeInput.className = 'obs-input char-type-input';
|
||
typeInput.placeholder = isAnimal ? 'Tipo (ex: Gatinho, Leão)' : 'Tipo (ex: Menino, Menina)';
|
||
typeInput.value = type;
|
||
typeInput.style.cssText = 'flex: 1; padding: 6px 10px; font-size: 0.85rem; margin: 0;';
|
||
row.appendChild(typeInput);
|
||
|
||
const nameInput = document.createElement('input');
|
||
nameInput.type = 'text';
|
||
nameInput.className = 'obs-input char-name-input';
|
||
nameInput.placeholder = 'Nome';
|
||
nameInput.value = name;
|
||
nameInput.style.cssText = 'flex: 1; padding: 6px 10px; font-size: 0.85rem; margin: 0;';
|
||
row.appendChild(nameInput);
|
||
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.type = 'button';
|
||
deleteBtn.className = 'btn-delete-char';
|
||
deleteBtn.innerHTML = '×';
|
||
deleteBtn.style.cssText = 'background: none; border: none; color: #ef4444; font-size: 1.3rem; cursor: pointer; padding: 0 4px; line-height: 1;';
|
||
deleteBtn.addEventListener('click', () => {
|
||
row.remove();
|
||
});
|
||
row.appendChild(deleteBtn);
|
||
|
||
comicsCharListContainer.appendChild(row);
|
||
};
|
||
|
||
if (btnAddHumanChar) {
|
||
btnAddHumanChar.addEventListener('click', () => {
|
||
addCharacterRow('', '', false);
|
||
});
|
||
}
|
||
|
||
if (btnAddAnimalChar) {
|
||
btnAddAnimalChar.addEventListener('click', () => {
|
||
addCharacterRow('', '', true);
|
||
});
|
||
}
|
||
|
||
// Exibir/ocultar cenário personalizado
|
||
if (comicsCenarioSelect) {
|
||
comicsCenarioSelect.addEventListener('change', () => {
|
||
if (comicsCenarioSelect.value === 'custom') {
|
||
comicsCenarioCustomInput.style.display = 'block';
|
||
} else {
|
||
comicsCenarioCustomInput.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- GERENCIADOR DE PROJETOS DE HQ ---
|
||
const startNewComicsProject = () => {
|
||
currentComicsProjectId = null;
|
||
currentComicsCharDescEnglish = '';
|
||
|
||
const modeContainer = document.getElementById('comicsGenerationModeContainer');
|
||
if (modeContainer) modeContainer.style.display = 'none';
|
||
const radioNew = document.querySelector('input[name="comicsGenMode"][value="new_story"]');
|
||
if (radioNew) radioNew.checked = true;
|
||
|
||
if (comicsProjectSelect) comicsProjectSelect.value = '';
|
||
if (comicsProjectTitleInput) comicsProjectTitleInput.value = '';
|
||
if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'none';
|
||
if (btnSaveComicsProject) btnSaveComicsProject.style.display = 'none';
|
||
|
||
comicsTemaInput.value = '';
|
||
comicsResultArea.style.display = 'none';
|
||
comicsGenerateLoader.style.display = 'none';
|
||
comicsVideoResult.style.display = 'none';
|
||
comicsVideoLoader.style.display = 'none';
|
||
btnGenerateComics.disabled = false;
|
||
if (comicsLoaderProgress) comicsLoaderProgress.style.width = '0%';
|
||
|
||
// Set active state for default quick options
|
||
selectedComicsQty = 5;
|
||
document.querySelectorAll('.btn-comics-qty').forEach(b => {
|
||
if (b.dataset.qty === '5') b.classList.add('active');
|
||
else b.classList.remove('active');
|
||
});
|
||
|
||
selectedComicsBubbles = true;
|
||
document.querySelectorAll('.btn-comics-bubbles').forEach(b => {
|
||
if (b.dataset.bubbles === 'true') b.classList.add('active');
|
||
else b.classList.remove('active');
|
||
});
|
||
|
||
selectedComicsRatio = '16:9';
|
||
document.querySelectorAll('.btn-comics-ratio').forEach(b => {
|
||
if (b.dataset.ratio === '16:9') b.classList.add('active');
|
||
else b.classList.remove('active');
|
||
});
|
||
|
||
if (comicsCenarioSelect) {
|
||
comicsCenarioSelect.value = 'no parque de diversões colorido';
|
||
if (comicsCenarioCustomInput) {
|
||
comicsCenarioCustomInput.style.display = 'none';
|
||
comicsCenarioCustomInput.value = '';
|
||
}
|
||
}
|
||
|
||
// Inicializar lista de personagens com padrão se estiver vazia
|
||
comicsCharListContainer.innerHTML = '';
|
||
addCharacterRow("Menino", "Lucas", false);
|
||
addCharacterRow("Menina", "Mariana", false);
|
||
|
||
generatedPanelsData = [];
|
||
comicsGridContainer.innerHTML = '';
|
||
};
|
||
|
||
const loadComicsProjectsList = async () => {
|
||
if (!comicsProjectSelect) return;
|
||
try {
|
||
const resp = await fetch('/api/comics/projects');
|
||
if (!resp.ok) throw new Error('Falha ao listar projetos');
|
||
const projects = await resp.json();
|
||
|
||
comicsProjectSelect.innerHTML = '<option value="">-- Carregar projeto salvo... --</option>';
|
||
projects.forEach(proj => {
|
||
const opt = document.createElement('option');
|
||
opt.value = proj.id;
|
||
opt.textContent = `${proj.titulo} (${new Date(proj.updated_at).toLocaleDateString('pt-BR')})`;
|
||
comicsProjectSelect.appendChild(opt);
|
||
});
|
||
|
||
if (currentComicsProjectId) {
|
||
comicsProjectSelect.value = currentComicsProjectId;
|
||
}
|
||
} catch (err) {
|
||
console.error('Erro ao listar projetos:', err);
|
||
}
|
||
};
|
||
|
||
const loadComicsProject = async (id) => {
|
||
if (!id) {
|
||
startNewComicsProject();
|
||
return;
|
||
}
|
||
try {
|
||
const resp = await fetch(`/api/comics/projects/${id}`);
|
||
if (!resp.ok) throw new Error('Falha ao carregar projeto');
|
||
const { project, panels } = await resp.json();
|
||
|
||
currentComicsProjectId = project.id;
|
||
currentComicsCharDescEnglish = project.character_description_english || '';
|
||
|
||
const modeContainer = document.getElementById('comicsGenerationModeContainer');
|
||
if (modeContainer) modeContainer.style.display = 'flex';
|
||
|
||
if (comicsProjectTitleInput) comicsProjectTitleInput.value = project.titulo || '';
|
||
comicsTemaInput.value = project.tema || '';
|
||
|
||
if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'block';
|
||
if (btnSaveComicsProject) btnSaveComicsProject.style.display = 'block';
|
||
|
||
// Carregar proporção
|
||
selectedComicsRatio = project.proporcao || '16:9';
|
||
document.querySelectorAll('.btn-comics-ratio').forEach(b => {
|
||
if (b.dataset.ratio === selectedComicsRatio) b.classList.add('active');
|
||
else b.classList.remove('active');
|
||
});
|
||
|
||
// Carregar cenário
|
||
if (comicsCenarioSelect) {
|
||
const standardOptions = ['no parque de diversões colorido', 'na sala de aula da escola infantil', 'em uma floresta encantada cheia de flores', 'no zoológico interagindo com animais', 'num jardim ensolarado de uma casa'];
|
||
if (standardOptions.includes(project.cenario)) {
|
||
comicsCenarioSelect.value = project.cenario;
|
||
if (comicsCenarioCustomInput) comicsCenarioCustomInput.style.display = 'none';
|
||
} else {
|
||
comicsCenarioSelect.value = 'custom';
|
||
if (comicsCenarioCustomInput) {
|
||
comicsCenarioCustomInput.style.display = 'block';
|
||
comicsCenarioCustomInput.value = project.cenario || '';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Carregar quantidade e balões baseados nos painéis retornados
|
||
selectedComicsQty = panels.length || 5;
|
||
document.querySelectorAll('.btn-comics-qty').forEach(b => {
|
||
if (parseInt(b.dataset.qty) === selectedComicsQty) b.classList.add('active');
|
||
else b.classList.remove('active');
|
||
});
|
||
|
||
const hasDialogue = panels.some(p => p.dialogue);
|
||
selectedComicsBubbles = hasDialogue;
|
||
document.querySelectorAll('.btn-comics-bubbles').forEach(b => {
|
||
if ((b.dataset.bubbles === 'true') === selectedComicsBubbles) b.classList.add('active');
|
||
else b.classList.remove('active');
|
||
});
|
||
|
||
// Carregar personagens
|
||
comicsCharListContainer.innerHTML = '';
|
||
if (project.character_description_global) {
|
||
try {
|
||
const chars = JSON.parse(project.character_description_global);
|
||
if (Array.isArray(chars)) {
|
||
chars.forEach(c => {
|
||
addCharacterRow(c.type || '', c.name || '', c.isAnimal || false);
|
||
});
|
||
}
|
||
} catch (e) {
|
||
addCharacterRow("Menino", "Lucas", false);
|
||
addCharacterRow("Menina", "Mariana", false);
|
||
}
|
||
} else {
|
||
addCharacterRow("Menino", "Lucas", false);
|
||
addCharacterRow("Menina", "Mariana", false);
|
||
}
|
||
|
||
// Carregar os painéis
|
||
generatedPanelsData = panels.map(p => ({
|
||
panel_number: p.panel_number,
|
||
imageUrl: p.image_url,
|
||
dialogue: p.dialogue || '',
|
||
image_prompt: p.image_prompt || ''
|
||
}));
|
||
|
||
if (generatedPanelsData.length > 0) {
|
||
comicsResultTitle.textContent = project.titulo || 'História em Quadrinhos';
|
||
comicsGridContainer.innerHTML = '';
|
||
|
||
generatedPanelsData.forEach((panel, index) => {
|
||
const panelCard = document.createElement('div');
|
||
panelCard.className = 'comic-panel-card';
|
||
|
||
const imgContainer = document.createElement('div');
|
||
imgContainer.className = `comic-panel-image-container ratio-${selectedComicsRatio.replace(':', '-')}`;
|
||
|
||
const img = document.createElement('img');
|
||
img.className = 'comic-panel-image';
|
||
img.src = panel.imageUrl;
|
||
img.alt = `Quadro ${panel.panel_number}`;
|
||
imgContainer.appendChild(img);
|
||
|
||
const badge = document.createElement('div');
|
||
badge.className = 'comic-panel-number-badge';
|
||
badge.textContent = panel.panel_number;
|
||
imgContainer.appendChild(badge);
|
||
|
||
panelCard.appendChild(imgContainer);
|
||
|
||
const caption = document.createElement('div');
|
||
caption.className = 'comic-caption-text';
|
||
caption.style.cssText = 'padding: 10px 14px; background: var(--bg-primary); border-top: 1px solid var(--border-light);';
|
||
|
||
const textarea = document.createElement('textarea');
|
||
textarea.className = 'comic-caption-input';
|
||
textarea.value = panel.dialogue || '';
|
||
textarea.placeholder = 'Digite a fala ou legenda do quadrinho...';
|
||
textarea.style.cssText = 'width: 100%; border: 1px solid var(--border-light); background: var(--bg-secondary); color: var(--text-primary); border-radius: 6px; padding: 8px; font-family: inherit; font-size: 0.85rem; resize: vertical; box-sizing: border-box; min-height: 50px;';
|
||
|
||
textarea.addEventListener('input', (e) => {
|
||
panel.dialogue = e.target.value;
|
||
if (generatedPanelsData[index]) {
|
||
generatedPanelsData[index].dialogue = e.target.value;
|
||
}
|
||
});
|
||
|
||
caption.appendChild(textarea);
|
||
panelCard.appendChild(caption);
|
||
|
||
comicsGridContainer.appendChild(panelCard);
|
||
});
|
||
|
||
comicsResultArea.style.display = 'flex';
|
||
} else {
|
||
comicsResultArea.style.display = 'none';
|
||
}
|
||
|
||
comicsVideoResult.style.display = 'none';
|
||
comicsVideoLoader.style.display = 'none';
|
||
btnGenerateComics.disabled = false;
|
||
|
||
} catch (err) {
|
||
console.error(err);
|
||
await showCustomAlert('Erro', 'Erro ao carregar projeto: ' + err.message);
|
||
}
|
||
};
|
||
|
||
const saveComicsProjectFlow = async (forceNew = false) => {
|
||
const titulo = comicsProjectTitleInput ? comicsProjectTitleInput.value.trim() : '';
|
||
if (!titulo) {
|
||
await showCustomAlert('Campo Obrigatório', 'Por favor, digite um título para salvar seu projeto.');
|
||
return;
|
||
}
|
||
|
||
const characters = [];
|
||
document.querySelectorAll('.comic-char-row').forEach(row => {
|
||
const typeInput = row.querySelector('.char-type-input');
|
||
const nameInput = row.querySelector('.char-name-input');
|
||
if (typeInput && nameInput) {
|
||
const type = typeInput.value.trim();
|
||
const name = nameInput.value.trim();
|
||
const isAnimal = row.querySelector('span').textContent === '🐾';
|
||
if (type || name) {
|
||
characters.push({ type, name, isAnimal });
|
||
}
|
||
}
|
||
});
|
||
|
||
let cenario = comicsCenarioSelect ? comicsCenarioSelect.value : 'no parque de diversões colorido';
|
||
if (cenario === 'custom' && comicsCenarioCustomInput) {
|
||
cenario = comicsCenarioCustomInput.value.trim();
|
||
}
|
||
|
||
const targetId = forceNew ? null : currentComicsProjectId;
|
||
|
||
const body = {
|
||
id: targetId,
|
||
titulo,
|
||
tema: comicsTemaInput.value.trim(),
|
||
cenario,
|
||
proporcao: selectedComicsRatio,
|
||
character_description_global: JSON.stringify(characters),
|
||
character_description_english: currentComicsCharDescEnglish || '',
|
||
panels: generatedPanelsData.map(p => ({
|
||
panel_number: p.panel_number,
|
||
image_url: p.imageUrl || p.image_url || '',
|
||
image_prompt: p.image_prompt || '',
|
||
dialogue: p.dialogue || ''
|
||
}))
|
||
};
|
||
|
||
try {
|
||
const resp = await fetch('/api/comics/projects', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body)
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
const err = await resp.json();
|
||
throw new Error(err.error || 'Erro ao salvar projeto');
|
||
}
|
||
|
||
const data = await resp.json();
|
||
currentComicsProjectId = data.id;
|
||
await showCustomAlert('Sucesso', 'Projeto salvo com sucesso!');
|
||
|
||
if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'block';
|
||
if (btnSaveComicsProject) btnSaveComicsProject.style.display = 'block';
|
||
await loadComicsProjectsList();
|
||
} catch (err) {
|
||
console.error(err);
|
||
await showCustomAlert('Erro', 'Erro ao salvar projeto: ' + err.message);
|
||
}
|
||
};
|
||
|
||
const saveComicsProject = () => saveComicsProjectFlow(false);
|
||
const saveNewComicsProject = () => saveComicsProjectFlow(true);
|
||
|
||
const deleteComicsProject = async () => {
|
||
if (!currentComicsProjectId) return;
|
||
const confirmed = await showCustomConfirm(
|
||
'Confirmar Exclusão',
|
||
'Tem certeza de que deseja excluir este projeto permanentemente?',
|
||
true
|
||
);
|
||
if (!confirmed) return;
|
||
|
||
try {
|
||
const resp = await fetch(`/api/comics/projects/${currentComicsProjectId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
|
||
if (!resp.ok) throw new Error('Falha ao excluir projeto');
|
||
|
||
await showCustomAlert('Excluído', 'Projeto excluído com sucesso!');
|
||
startNewComicsProject();
|
||
await loadComicsProjectsList();
|
||
} catch (err) {
|
||
console.error(err);
|
||
await showCustomAlert('Erro', 'Erro ao excluir projeto: ' + err.message);
|
||
}
|
||
};
|
||
|
||
// Abrir e fechar modal Fábrica de Quadrinhos
|
||
const openComicsModal = () => {
|
||
fabricaQuadrinhosModal.style.display = 'flex';
|
||
startNewComicsProject();
|
||
loadComicsProjectsList();
|
||
};
|
||
|
||
// Listeners do Gerenciador de Projetos
|
||
if (comicsProjectSelect) {
|
||
comicsProjectSelect.addEventListener('change', (e) => {
|
||
loadComicsProject(e.target.value);
|
||
});
|
||
}
|
||
|
||
if (btnNewComicsProject) {
|
||
btnNewComicsProject.addEventListener('click', async () => {
|
||
const confirmed = await showCustomConfirm(
|
||
'Novo Projeto',
|
||
'Deseja iniciar um novo projeto? Alterações não salvas serão perdidas.'
|
||
);
|
||
if (confirmed) {
|
||
startNewComicsProject();
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnSaveComicsProject) {
|
||
btnSaveComicsProject.addEventListener('click', saveComicsProject);
|
||
}
|
||
|
||
if (btnSaveNewComicsProject) {
|
||
btnSaveNewComicsProject.addEventListener('click', saveNewComicsProject);
|
||
}
|
||
|
||
if (btnDeleteComicsProject) {
|
||
btnDeleteComicsProject.addEventListener('click', deleteComicsProject);
|
||
}
|
||
|
||
if (barBtnComics) {
|
||
barBtnComics.addEventListener('click', openComicsModal);
|
||
}
|
||
|
||
if (btnCloseComicsModal) {
|
||
btnCloseComicsModal.addEventListener('click', () => {
|
||
fabricaQuadrinhosModal.style.display = 'none';
|
||
if (comicsVideoPlayer) comicsVideoPlayer.pause();
|
||
});
|
||
}
|
||
|
||
// Fechar modal clicando fora
|
||
window.addEventListener('click', (e) => {
|
||
if (e.target === fabricaQuadrinhosModal) {
|
||
fabricaQuadrinhosModal.style.display = 'none';
|
||
if (comicsVideoPlayer) comicsVideoPlayer.pause();
|
||
}
|
||
});
|
||
|
||
// Helper para converter URL de imagem local em base64
|
||
const getBase64Image = async (imgUrl) => {
|
||
return new Promise((resolve, reject) => {
|
||
const img = new Image();
|
||
img.crossOrigin = 'Anonymous';
|
||
img.onload = () => {
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = img.width;
|
||
canvas.height = img.height;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.drawImage(img, 0, 0);
|
||
resolve(canvas.toDataURL('image/jpeg'));
|
||
};
|
||
img.onerror = (e) => reject(new Error('Falha ao carregar imagem para o PDF: ' + imgUrl));
|
||
img.src = imgUrl;
|
||
});
|
||
};
|
||
|
||
// Ação de geração da história e imagens
|
||
if (btnGenerateComics) {
|
||
btnGenerateComics.addEventListener('click', async () => {
|
||
const tema = comicsTemaInput.value.trim();
|
||
if (!tema) {
|
||
await showCustomAlert('Campo Requerido', 'Por favor, digite um tema para a história.');
|
||
return;
|
||
}
|
||
|
||
// Reunir personagens dinamicamente
|
||
const personagens = [];
|
||
document.querySelectorAll('.comic-char-row').forEach(row => {
|
||
const type = row.querySelector('.char-type-input').value.trim();
|
||
const name = row.querySelector('.char-name-input').value.trim();
|
||
if (type && name) {
|
||
personagens.push(`${type} chamado(a) ${name}`);
|
||
}
|
||
});
|
||
|
||
if (personagens.length === 0) {
|
||
await showCustomAlert('Personagens Faltando', 'Adicione e preencha pelo menos um personagem principal.');
|
||
return;
|
||
}
|
||
|
||
// Definir cenário
|
||
let cenario = comicsCenarioSelect.value;
|
||
if (cenario === 'custom') {
|
||
cenario = comicsCenarioCustomInput.value.trim() || 'em um local bonito';
|
||
}
|
||
|
||
btnGenerateComics.disabled = true;
|
||
comicsGenerateLoader.style.display = 'flex';
|
||
comicsResultArea.style.display = 'none';
|
||
comicsVideoResult.style.display = 'none';
|
||
comicsLoaderProgress.style.width = '5%';
|
||
comicsLoaderText.textContent = 'Escrevendo o roteiro e diálogos dos quadrinhos...';
|
||
|
||
try {
|
||
// 1. Gerar Roteiro
|
||
const genMode = currentComicsProjectId ? (document.querySelector('input[name="comicsGenMode"]:checked')?.value || 'new_story') : 'new_story';
|
||
|
||
const requestBody = {
|
||
tema,
|
||
quantidade: selectedComicsQty,
|
||
baloes: selectedComicsBubbles,
|
||
proporcao: selectedComicsRatio,
|
||
personagens,
|
||
cenario
|
||
};
|
||
|
||
if (currentComicsProjectId) {
|
||
requestBody.continuationType = genMode;
|
||
requestBody.character_description = currentComicsCharDescEnglish;
|
||
requestBody.existingPanels = generatedPanelsData.map(p => ({
|
||
panel_number: p.panel_number,
|
||
image_url: p.imageUrl || p.image_url || '',
|
||
image_prompt: p.image_prompt || '',
|
||
dialogue: p.dialogue || ''
|
||
}));
|
||
}
|
||
|
||
const scriptResp = await fetch('/api/comics/generate-script', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(requestBody)
|
||
});
|
||
|
||
if (!scriptResp.ok) {
|
||
const err = await scriptResp.json();
|
||
throw new Error(err.error || 'Erro ao planejar roteiro');
|
||
}
|
||
|
||
const scriptData = await scriptResp.json();
|
||
comicsLoaderProgress.style.width = '15%';
|
||
|
||
if (scriptData.character_description) {
|
||
currentComicsCharDescEnglish = scriptData.character_description;
|
||
}
|
||
|
||
// 2. Gerar cada quadrinho sequencialmente
|
||
if (genMode !== 'extend') {
|
||
generatedPanelsData = [];
|
||
}
|
||
const totalPanels = scriptData.panels.length;
|
||
|
||
for (let i = 0; i < totalPanels; i++) {
|
||
const panel = scriptData.panels[i];
|
||
const progressPercent = 15 + Math.floor((i / totalPanels) * 80);
|
||
comicsLoaderProgress.style.width = `${progressPercent}%`;
|
||
comicsLoaderText.textContent = `Ilustrando quadrinho ${i + 1} de ${totalPanels}...`;
|
||
|
||
// Injetamos a proporção e estilo de forma reforçada no prompt
|
||
const fullPrompt = `${panel.image_prompt}, high resolution children book illustration, cute Pixar style, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`;
|
||
|
||
const frameResp = await fetch('/api/comics/generate-frame', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ prompt: fullPrompt })
|
||
});
|
||
|
||
if (!frameResp.ok) {
|
||
const err = await frameResp.json();
|
||
throw new Error(`Erro no quadrinho ${i + 1}: ${err.error}`);
|
||
}
|
||
|
||
const frameData = await frameResp.json();
|
||
generatedPanelsData.push({
|
||
panel_number: panel.panel_number,
|
||
imageUrl: frameData.imageUrl,
|
||
dialogue: panel.dialogue || '',
|
||
image_prompt: panel.image_prompt || ''
|
||
});
|
||
}
|
||
|
||
// Renderizar quadrinhos na tela (todos, incluindo os anteriores caso tenha sido extensão)
|
||
comicsResultTitle.textContent = scriptData.title || comicsResultTitle.textContent || 'Fábrica de Quadrinhos Camila';
|
||
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 || panel.image_url;
|
||
img.alt = `Quadro ${panel.panel_number}`;
|
||
imgContainer.appendChild(img);
|
||
|
||
// Selo do número
|
||
const badge = document.createElement('div');
|
||
badge.className = 'comic-panel-number-badge';
|
||
badge.textContent = panel.panel_number;
|
||
imgContainer.appendChild(badge);
|
||
|
||
panelCard.appendChild(imgContainer);
|
||
|
||
const caption = document.createElement('div');
|
||
caption.className = 'comic-caption-text';
|
||
caption.style.cssText = 'padding: 10px 14px; background: var(--bg-primary); border-top: 1px solid var(--border-light);';
|
||
|
||
const textarea = document.createElement('textarea');
|
||
textarea.className = 'comic-caption-input';
|
||
textarea.value = panel.dialogue || '';
|
||
textarea.placeholder = 'Digite a fala ou legenda do quadrinho...';
|
||
textarea.style.cssText = 'width: 100%; border: 1px solid var(--border-light); background: var(--bg-secondary); color: var(--text-primary); border-radius: 6px; padding: 8px; font-family: inherit; font-size: 0.85rem; resize: vertical; box-sizing: border-box; min-height: 50px;';
|
||
|
||
textarea.addEventListener('input', (e) => {
|
||
panel.dialogue = e.target.value;
|
||
if (generatedPanelsData[index]) {
|
||
generatedPanelsData[index].dialogue = e.target.value;
|
||
}
|
||
});
|
||
|
||
caption.appendChild(textarea);
|
||
panelCard.appendChild(caption);
|
||
|
||
comicsGridContainer.appendChild(panelCard);
|
||
});
|
||
|
||
// Se for uma nova história derivada de um projeto antigo, limpamos o ID para virar um novo projeto independente ao salvar
|
||
if (genMode === 'new_story' && currentComicsProjectId) {
|
||
currentComicsProjectId = null;
|
||
if (comicsProjectSelect) comicsProjectSelect.value = '';
|
||
const modeContainer = document.getElementById('comicsGenerationModeContainer');
|
||
if (modeContainer) modeContainer.style.display = 'none';
|
||
if (comicsProjectTitleInput) {
|
||
comicsProjectTitleInput.value = scriptData.title || (comicsProjectTitleInput.value + ' (Derivado)');
|
||
}
|
||
if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'none';
|
||
}
|
||
|
||
comicsGenerateLoader.style.display = 'none';
|
||
comicsResultArea.style.display = 'flex';
|
||
btnGenerateComics.disabled = false;
|
||
|
||
} catch (err) {
|
||
console.error('Erro ao fabricar quadrinhos:', err);
|
||
await showCustomAlert('Erro', 'Erro ao fabricar quadrinhos: ' + err.message);
|
||
comicsGenerateLoader.style.display = 'none';
|
||
btnGenerateComics.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Exportar PDF Retrato (1 por página)
|
||
if (btnExportComicsPDFPortrait) {
|
||
btnExportComicsPDFPortrait.addEventListener('click', async () => {
|
||
if (generatedPanelsData.length === 0) return;
|
||
btnExportComicsPDFPortrait.disabled = true;
|
||
btnExportComicsPDFPortrait.textContent = '⏳ Montando PDF...';
|
||
|
||
try {
|
||
const { jsPDF } = window.jspdf;
|
||
const doc = new jsPDF('p', 'pt', 'a4');
|
||
const title = comicsResultTitle.textContent || 'História em Quadrinhos';
|
||
|
||
for (let i = 0; i < generatedPanelsData.length; i++) {
|
||
const panel = generatedPanelsData[i];
|
||
if (i > 0) doc.addPage();
|
||
|
||
// Título centralizado
|
||
doc.setFont('helvetica', 'bold');
|
||
doc.setFontSize(18);
|
||
doc.setTextColor(30, 41, 59);
|
||
doc.text(title, 297, 50, { align: 'center' });
|
||
|
||
// Imagem
|
||
const base64Img = await getBase64Image(panel.imageUrl);
|
||
const imgWidth = 515;
|
||
const imgHeight = selectedComicsRatio === '16:9' ? 290 : 386;
|
||
doc.addImage(base64Img, 'JPEG', 40, 80, imgWidth, imgHeight);
|
||
|
||
// Altura esticada para a caixa de diálogo (160pt de altura - aprox dobro)
|
||
const textHeight = 160;
|
||
const textY = 80 + imgHeight;
|
||
|
||
// Caixa de diálogo com fundo cinza claro e borda integrada
|
||
doc.setFillColor(248, 250, 252);
|
||
doc.rect(40, textY, imgWidth, textHeight, 'F');
|
||
|
||
// Moldura unificada ao redor de toda a imagem + texto
|
||
doc.setDrawColor(30, 41, 59);
|
||
doc.setLineWidth(2);
|
||
doc.rect(40, 80, imgWidth, imgHeight + textHeight, 'D');
|
||
|
||
// Linha divisória entre a imagem e a caixa de texto
|
||
doc.line(40, textY, 555, textY);
|
||
|
||
// Texto maior e mais legível (dobro do tamanho relativo - 15pt)
|
||
if (panel.dialogue) {
|
||
doc.setFont('helvetica', 'normal');
|
||
doc.setFontSize(15);
|
||
doc.setTextColor(15, 23, 42);
|
||
|
||
const splitText = doc.splitTextToSize(panel.dialogue, imgWidth - 40);
|
||
doc.text(splitText, 60, textY + 35);
|
||
}
|
||
|
||
// Círculo discreto com o número do quadrinho no canto superior esquerdo da imagem
|
||
doc.setFillColor(254, 240, 138);
|
||
doc.circle(65, 105, 12, 'F');
|
||
doc.setDrawColor(30, 41, 59);
|
||
doc.setLineWidth(1.5);
|
||
doc.circle(65, 105, 12, 'D');
|
||
|
||
doc.setFont('helvetica', 'bold');
|
||
doc.setFontSize(10);
|
||
doc.setTextColor(30, 41, 59);
|
||
doc.text(String(panel.panel_number), 65, 109, { align: 'center' });
|
||
|
||
// Rodapé
|
||
doc.setFont('helvetica', 'italic');
|
||
doc.setFontSize(9);
|
||
doc.setTextColor(148, 163, 184);
|
||
doc.text(`Criado com PedaGog - Quadro ${panel.panel_number} de ${generatedPanelsData.length}`, 297, 815, { align: 'center' });
|
||
}
|
||
|
||
doc.save(`quadrinhos_${title.replace(/\s+/g, '_').toLowerCase()}.pdf`);
|
||
} catch (error) {
|
||
console.error('Erro ao gerar PDF Retrato:', error);
|
||
alert('Erro ao gerar PDF: ' + error.message);
|
||
} finally {
|
||
btnExportComicsPDFPortrait.disabled = false;
|
||
btnExportComicsPDFPortrait.textContent = '📄 PDF (1 por página - Retrato)';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Exportar PDF Paisagem (2 por página)
|
||
if (btnExportComicsPDFLandscape) {
|
||
btnExportComicsPDFLandscape.addEventListener('click', async () => {
|
||
if (generatedPanelsData.length === 0) return;
|
||
btnExportComicsPDFLandscape.disabled = true;
|
||
btnExportComicsPDFLandscape.textContent = '⏳ Montando PDF...';
|
||
|
||
try {
|
||
const { jsPDF } = window.jspdf;
|
||
const doc = new jsPDF('l', 'pt', 'a4');
|
||
const title = comicsResultTitle.textContent || 'História em Quadrinhos';
|
||
|
||
const totalPanels = generatedPanelsData.length;
|
||
const totalPages = Math.ceil(totalPanels / 2);
|
||
|
||
for (let page = 0; page < totalPages; page++) {
|
||
if (page > 0) doc.addPage();
|
||
|
||
// Título centralizado no topo da página paisagem
|
||
doc.setFont('helvetica', 'bold');
|
||
doc.setFontSize(18);
|
||
doc.setTextColor(30, 41, 59);
|
||
doc.text(`${title} - Página ${page + 1} de ${totalPages}`, 421, 45, { align: 'center' });
|
||
|
||
// Renderizar até dois quadrinhos lado a lado
|
||
for (let col = 0; col < 2; col++) {
|
||
const index = page * 2 + col;
|
||
if (index >= totalPanels) break;
|
||
|
||
const panel = generatedPanelsData[index];
|
||
const startX = col === 0 ? 40 : 426;
|
||
const cardWidth = 376;
|
||
|
||
// Proporções exatas mantidas
|
||
const imgHeight = selectedComicsRatio === '16:9' ? 211 : 282;
|
||
const textHeight = 140; // Dobro de tamanho (antes era 65)
|
||
|
||
// Imagem do quadrinho
|
||
const base64Img = await getBase64Image(panel.imageUrl);
|
||
doc.addImage(base64Img, 'JPEG', startX, 70, cardWidth, imgHeight);
|
||
|
||
// Fundo da caixa de texto
|
||
const textY = 70 + imgHeight;
|
||
doc.setFillColor(248, 250, 252);
|
||
doc.rect(startX, textY, cardWidth, textHeight, 'F');
|
||
|
||
// Moldura unificada ao redor do quadro (imagem + texto)
|
||
doc.setDrawColor(30, 41, 59);
|
||
doc.setLineWidth(2);
|
||
doc.rect(startX, 70, cardWidth, imgHeight + textHeight, 'D');
|
||
|
||
// Linha separando a imagem da caixa de texto
|
||
doc.line(startX, textY, startX + cardWidth, textY);
|
||
|
||
// Legenda formatada em tamanho maior (13pt)
|
||
if (panel.dialogue) {
|
||
doc.setFont('helvetica', 'normal');
|
||
doc.setFontSize(13);
|
||
doc.setTextColor(15, 23, 42);
|
||
|
||
const splitText = doc.splitTextToSize(panel.dialogue, cardWidth - 30);
|
||
doc.text(splitText, startX + 15, textY + 30);
|
||
}
|
||
|
||
// Círculo discreto com o numeral do quadro no canto superior esquerdo da imagem
|
||
doc.setFillColor(254, 240, 138);
|
||
doc.circle(startX + 20, 90, 11, 'F');
|
||
doc.setDrawColor(30, 41, 59);
|
||
doc.setLineWidth(1.5);
|
||
doc.circle(startX + 20, 90, 11, 'D');
|
||
|
||
doc.setFont('helvetica', 'bold');
|
||
doc.setFontSize(9);
|
||
doc.setTextColor(30, 41, 59);
|
||
doc.text(String(panel.panel_number), startX + 20, 93, { align: 'center' });
|
||
}
|
||
}
|
||
|
||
doc.save(`quadrinhos_${title.replace(/\s+/g, '_').toLowerCase()}_paisagem.pdf`);
|
||
} catch (error) {
|
||
console.error('Erro ao gerar PDF Paisagem:', error);
|
||
alert('Erro ao gerar PDF: ' + error.message);
|
||
} finally {
|
||
btnExportComicsPDFLandscape.disabled = false;
|
||
btnExportComicsPDFLandscape.textContent = '📄 PDF (2 por página - Paisagem)';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Compilar Vídeo de Apresentação (FFmpeg)
|
||
if (btnCompileComicsVideo) {
|
||
btnCompileComicsVideo.addEventListener('click', async () => {
|
||
if (generatedPanelsData.length === 0) return;
|
||
|
||
btnCompileComicsVideo.disabled = true;
|
||
comicsVideoLoader.style.display = 'flex';
|
||
comicsVideoResult.style.display = 'none';
|
||
|
||
try {
|
||
const response = await fetch('/api/comics/generate-video', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
frames: generatedPanelsData,
|
||
musicSelection: comicsVideoMusic.value,
|
||
frameDuration: comicsVideoDuration.value
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const err = await response.json();
|
||
throw new Error(err.error || 'Erro na compilação do vídeo');
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
comicsVideoPlayer.src = data.videoUrl;
|
||
comicsVideoDownloadBtn.href = data.videoUrl;
|
||
|
||
comicsVideoResult.style.display = 'flex';
|
||
comicsVideoLoader.style.display = 'none';
|
||
btnCompileComicsVideo.disabled = false;
|
||
|
||
} catch (err) {
|
||
console.error('Erro ao gerar vídeo dos quadrinhos:', err);
|
||
alert('Erro ao compilar o vídeo: ' + err.message);
|
||
comicsVideoLoader.style.display = 'none';
|
||
btnCompileComicsVideo.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// ============================================================
|
||
// VIDEOMIND (TRANSCRIÇÃO E ANÁLISE DE VÍDEO PEDAGÓGICO)
|
||
// ============================================================
|
||
const videoMindModal = document.getElementById('videoMindModal');
|
||
const btnCloseVideoMindModal = document.getElementById('btnCloseVideoMindModal');
|
||
const barBtnVideoMind = document.getElementById('barBtnVideoMind');
|
||
|
||
const btnGenerateVideoMind = document.getElementById('btnGenerateVideoMind');
|
||
const videoMindUrlInput = document.getElementById('videoMindUrlInput');
|
||
const videoMindLoader = document.getElementById('videoMindLoader');
|
||
const videoMindLoaderText = document.getElementById('videoMindLoaderText');
|
||
const videoMindResultArea = document.getElementById('videoMindResultArea');
|
||
|
||
const videoMindThumbnail = document.getElementById('videoMindThumbnail');
|
||
const videoMindTitle = document.getElementById('videoMindTitle');
|
||
const videoMindAuthor = document.getElementById('videoMindAuthor');
|
||
const videoMindReportArea = document.getElementById('videoMindReportArea');
|
||
|
||
const btnCopyVideoMindReport = document.getElementById('btnCopyVideoMindReport');
|
||
const btnDownloadVideoMindReport = document.getElementById('btnDownloadVideoMindReport');
|
||
const btnPdfVideoMindReport = document.getElementById('btnPdfVideoMindReport');
|
||
|
||
let selectedVideoMindLines = 20;
|
||
let activeVideoMindReport = null;
|
||
|
||
// Listener para selecionar o tamanho do resumo
|
||
document.querySelectorAll('#videoMindModal .music-option-grid .btn-videomind-lines').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('#videoMindModal .music-option-grid .btn-videomind-lines').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
selectedVideoMindLines = parseInt(btn.dataset.lines) || 20;
|
||
});
|
||
});
|
||
|
||
// Abrir modal do VideoMind
|
||
if (barBtnVideoMind) {
|
||
barBtnVideoMind.addEventListener('click', () => {
|
||
videoMindModal.style.display = 'flex';
|
||
videoMindUrlInput.value = '';
|
||
videoMindResultArea.style.display = 'none';
|
||
videoMindReportArea.innerHTML = '';
|
||
if (videoMindLoader) videoMindLoader.style.display = 'none';
|
||
btnGenerateVideoMind.disabled = false;
|
||
activeVideoMindReport = null;
|
||
});
|
||
}
|
||
|
||
// Fechar modal
|
||
if (btnCloseVideoMindModal) {
|
||
btnCloseVideoMindModal.addEventListener('click', () => {
|
||
videoMindModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
window.addEventListener('click', (e) => {
|
||
if (e.target === videoMindModal) {
|
||
videoMindModal.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// Ação de analisar vídeo
|
||
if (btnGenerateVideoMind) {
|
||
btnGenerateVideoMind.addEventListener('click', async () => {
|
||
const url = videoMindUrlInput.value.trim();
|
||
const pastedTranscriptInput = document.getElementById('videoMindPastedTranscriptInput');
|
||
const pastedTranscript = pastedTranscriptInput ? pastedTranscriptInput.value.trim() : '';
|
||
|
||
if (!url) {
|
||
alert('Por favor, insira o link de um vídeo do YouTube.');
|
||
return;
|
||
}
|
||
|
||
btnGenerateVideoMind.disabled = true;
|
||
videoMindLoader.style.display = 'flex';
|
||
videoMindResultArea.style.display = 'none';
|
||
videoMindLoaderText.textContent = 'Buscando metadados do vídeo...';
|
||
|
||
let statusMsgIdx = 0;
|
||
const statusMessages = [
|
||
'Buscando legenda automática do vídeo...',
|
||
'Carregando a transcrição completa...',
|
||
'Enviando transcrição para a IA...',
|
||
'Pedagoga IA analisando o conteúdo...',
|
||
'Alinhando temas do vídeo com a BNCC...',
|
||
'Gerando resumo de ' + selectedVideoMindLines + ' linhas...',
|
||
'Criando propostas de atividades pedagógicas...',
|
||
'Finalizando o parecer pedagógico...'
|
||
];
|
||
|
||
const statusInterval = setInterval(() => {
|
||
if (statusMsgIdx < statusMessages.length) {
|
||
videoMindLoaderText.textContent = statusMessages[statusMsgIdx];
|
||
statusMsgIdx++;
|
||
} else {
|
||
videoMindLoaderText.textContent = 'Ajustando formatação da análise...';
|
||
}
|
||
}, 4000);
|
||
|
||
try {
|
||
const response = await fetch('/api/videomind/analyze', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
url,
|
||
lines: selectedVideoMindLines,
|
||
pastedTranscript
|
||
})
|
||
});
|
||
|
||
clearInterval(statusInterval);
|
||
|
||
if (!response.ok) {
|
||
const errData = await response.json();
|
||
throw new Error(errData.error || 'Erro desconhecido');
|
||
}
|
||
|
||
const data = await response.json();
|
||
activeVideoMindReport = data;
|
||
|
||
// Preencher informações do vídeo
|
||
videoMindTitle.textContent = data.metadata.title;
|
||
videoMindAuthor.textContent = 'Canal: ' + data.metadata.author;
|
||
videoMindThumbnail.src = data.metadata.thumbnail;
|
||
|
||
// Renderizar Markdown
|
||
if (window.marked) {
|
||
videoMindReportArea.innerHTML = marked.parse(data.report);
|
||
} else {
|
||
videoMindReportArea.textContent = data.report;
|
||
}
|
||
|
||
videoMindLoader.style.display = 'none';
|
||
videoMindResultArea.style.display = 'flex';
|
||
|
||
} catch (err) {
|
||
clearInterval(statusInterval);
|
||
console.error('Erro no VideoMind:', err);
|
||
alert('Erro ao analisar vídeo: ' + err.message);
|
||
videoMindLoader.style.display = 'none';
|
||
btnGenerateVideoMind.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Copiar Parecer
|
||
if (btnCopyVideoMindReport) {
|
||
btnCopyVideoMindReport.addEventListener('click', () => {
|
||
if (activeVideoMindReport && activeVideoMindReport.report) {
|
||
navigator.clipboard.writeText(activeVideoMindReport.report)
|
||
.then(() => {
|
||
btnCopyVideoMindReport.textContent = '✅ Copiado!';
|
||
setTimeout(() => {
|
||
btnCopyVideoMindReport.textContent = '📋 Copiar Parecer';
|
||
}, 2000);
|
||
})
|
||
.catch(err => {
|
||
console.error('Falha ao copiar:', err);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// Baixar relatório TXT
|
||
if (btnDownloadVideoMindReport) {
|
||
btnDownloadVideoMindReport.addEventListener('click', () => {
|
||
if (activeVideoMindReport && activeVideoMindReport.report) {
|
||
const title = activeVideoMindReport.metadata.title || 'analise_video';
|
||
const blob = new Blob([activeVideoMindReport.report], { type: 'text/plain;charset=utf-8' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `VideoMind_${title.replace(/\s+/g, '_').toLowerCase()}.txt`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Baixar relatório PDF
|
||
if (btnPdfVideoMindReport) {
|
||
btnPdfVideoMindReport.addEventListener('click', () => {
|
||
if (!activeVideoMindReport) return;
|
||
|
||
const printWindow = window.open('', '_blank');
|
||
printWindow.document.write(`
|
||
<html>
|
||
<head>
|
||
<title>VideoMind - Parecer Pedagógico</title>
|
||
<style>
|
||
body { font-family: 'Outfit', 'Inter', -apple-system, sans-serif; padding: 40px; color: #333; line-height: 1.6; }
|
||
h1, h2, h3 { color: #0891b2; margin-top: 24px; margin-bottom: 12px; }
|
||
h1 { border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; font-size: 22px; }
|
||
h2 { font-size: 18px; border-bottom: 1px solid #f1f5f9; padding-bottom: 6px; }
|
||
.meta-box { background: #f8fafc; padding: 16px; border-radius: 8px; margin-bottom: 24px; font-size: 14px; border: 1px solid #e2e8f0; display: flex; gap: 16px; align-items: center; }
|
||
.meta-info { display: flex; flex-direction: column; gap: 4px; }
|
||
.meta-label { font-weight: bold; color: #475569; }
|
||
p { margin-bottom: 14px; text-align: justify; }
|
||
ul, ol { margin-bottom: 14px; padding-left: 20px; }
|
||
li { margin-bottom: 6px; }
|
||
@media print {
|
||
body { padding: 0; }
|
||
button { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>🎬 VideoMind - Análise & Parecer Pedagógico</h1>
|
||
<div class="meta-box">
|
||
<img src="${activeVideoMindReport.metadata.thumbnail}" style="width: 100px; aspect-ratio: 16/9; object-fit: cover; border-radius: 6px;">
|
||
<div class="meta-info">
|
||
<div><span class="meta-label">Vídeo:</span> <span>${activeVideoMindReport.metadata.title}</span></div>
|
||
<div><span class="meta-label">Canal:</span> <span>${activeVideoMindReport.metadata.author}</span></div>
|
||
<div><span class="meta-label">Resumo Escolhido:</span> <span>${selectedVideoMindLines} Linhas</span></div>
|
||
</div>
|
||
</div>
|
||
<div>${window.marked ? marked.parse(activeVideoMindReport.report) : activeVideoMindReport.report}</div>
|
||
<script>
|
||
window.onload = function() {
|
||
window.print();
|
||
setTimeout(() => { window.close(); }, 500);
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|
||
`);
|
||
});
|
||
}
|
||
|
||
// ============================================================
|
||
// GARATUJAS (ANALISADOR DE TRAÇADO INFANTIL)
|
||
// ============================================================
|
||
const garatujasModal = document.getElementById('garatujasModal');
|
||
const btnCloseGaratujasModal = document.getElementById('btnCloseGaratujasModal');
|
||
const barBtnGaratujas = document.getElementById('barBtnGaratujas');
|
||
|
||
const tabGaratujasNova = document.getElementById('tabGaratujasNova');
|
||
const tabGaratujasHistorico = document.getElementById('tabGaratujasHistorico');
|
||
const contentGaratujasNova = document.getElementById('contentGaratujasNova');
|
||
const contentGaratujasHistorico = document.getElementById('contentGaratujasHistorico');
|
||
|
||
const garatujasAlunoInput = document.getElementById('garatujasAlunoInput');
|
||
const garatujasIdadeInput = document.getElementById('garatujasIdadeInput');
|
||
const garatujaFileInput = document.getElementById('garatujaFileInput');
|
||
const btnUploadGaratuja = document.getElementById('btnUploadGaratuja');
|
||
const garatujaFileStatus = document.getElementById('garatujaFileStatus');
|
||
const garatujaPreviewContainer = document.getElementById('garatujaPreviewContainer');
|
||
const garatujaImgPreview = document.getElementById('garatujaImgPreview');
|
||
|
||
const btnGenerateGaratuja = document.getElementById('btnGenerateGaratuja');
|
||
const garatujaLoader = document.getElementById('garatujaLoader');
|
||
const garatujaLoaderText = document.getElementById('garatujaLoaderText');
|
||
const garatujaResultArea = document.getElementById('garatujaResultArea');
|
||
const garatujaReportArea = document.getElementById('garatujaReportArea');
|
||
|
||
const btnCopyGaratujaReport = document.getElementById('btnCopyGaratujaReport');
|
||
const btnPdfGaratujaReport = document.getElementById('btnPdfGaratujaReport');
|
||
const garatujasListContainer = document.getElementById('garatujasListContainer');
|
||
const garatujasNoData = document.getElementById('garatujasNoData');
|
||
|
||
let activeGaratujaReport = null;
|
||
|
||
// Abas do Garatujas
|
||
if (tabGaratujasNova) {
|
||
tabGaratujasNova.addEventListener('click', () => {
|
||
tabGaratujasNova.classList.add('active');
|
||
tabGaratujasHistorico.classList.remove('active');
|
||
contentGaratujasNova.style.display = 'block';
|
||
contentGaratujasHistorico.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
if (tabGaratujasHistorico) {
|
||
tabGaratujasHistorico.addEventListener('click', () => {
|
||
tabGaratujasHistorico.classList.add('active');
|
||
tabGaratujasNova.classList.remove('active');
|
||
contentGaratujasNova.style.display = 'none';
|
||
contentGaratujasHistorico.style.display = 'block';
|
||
loadGaratujasHistory();
|
||
});
|
||
}
|
||
|
||
// Abrir Modal
|
||
if (barBtnGaratujas) {
|
||
barBtnGaratujas.addEventListener('click', () => {
|
||
garatujasModal.style.display = 'flex';
|
||
garatujasAlunoInput.value = '';
|
||
garatujaFileInput.value = '';
|
||
garatujaFileStatus.textContent = 'Nenhum arquivo selecionado';
|
||
garatujaPreviewContainer.style.display = 'none';
|
||
garatujaResultArea.style.display = 'none';
|
||
garatujaLoader.style.display = 'none';
|
||
btnGenerateGaratuja.disabled = false;
|
||
activeGaratujaReport = null;
|
||
if (tabGaratujasNova) tabGaratujasNova.click();
|
||
});
|
||
}
|
||
|
||
// Fechar Modal
|
||
if (btnCloseGaratujasModal) {
|
||
btnCloseGaratujasModal.addEventListener('click', () => {
|
||
garatujasModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
window.addEventListener('click', (e) => {
|
||
if (e.target === garatujasModal) {
|
||
garatujasModal.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// Upload trigger
|
||
if (btnUploadGaratuja) {
|
||
btnUploadGaratuja.addEventListener('click', () => {
|
||
garatujaFileInput.click();
|
||
});
|
||
}
|
||
|
||
if (garatujaFileInput) {
|
||
garatujaFileInput.addEventListener('change', () => {
|
||
const file = garatujaFileInput.files[0];
|
||
if (file) {
|
||
garatujaFileStatus.textContent = file.name;
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
garatujaImgPreview.src = e.target.result;
|
||
garatujaPreviewContainer.style.display = 'block';
|
||
};
|
||
reader.readAsDataURL(file);
|
||
} else {
|
||
garatujaFileStatus.textContent = 'Nenhum arquivo selecionado';
|
||
garatujaPreviewContainer.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Gerar Análise
|
||
if (btnGenerateGaratuja) {
|
||
btnGenerateGaratuja.addEventListener('click', async () => {
|
||
const alunoName = garatujasAlunoInput.value.trim();
|
||
const alunoIdade = garatujasIdadeInput ? garatujasIdadeInput.value.trim() : '';
|
||
const file = garatujaFileInput.files[0];
|
||
|
||
if (!alunoName) {
|
||
alert('Por favor, informe o nome do aluno.');
|
||
return;
|
||
}
|
||
if (!file) {
|
||
alert('Por favor, selecione a foto do desenho.');
|
||
return;
|
||
}
|
||
|
||
btnGenerateGaratuja.disabled = true;
|
||
garatujaLoader.style.display = 'flex';
|
||
garatujaResultArea.style.display = 'none';
|
||
garatujaLoaderText.textContent = 'Enviando imagem pedagógica...';
|
||
|
||
const formData = new FormData();
|
||
formData.append('alunoName', alunoName);
|
||
if (alunoIdade) formData.append('alunoIdade', alunoIdade);
|
||
formData.append('image', file);
|
||
|
||
let statusMsgIdx = 0;
|
||
const statusMessages = [
|
||
'Carregando desenho infantil...',
|
||
'IA escaneando os traços e formas...',
|
||
'Analisando simetria e coordenação motora...',
|
||
'Definindo estágio evolutivo do desenho...',
|
||
'Criando sugestões de atividades formativas...',
|
||
'Finalizando laudo psicopedagógico...'
|
||
];
|
||
|
||
const statusInterval = setInterval(() => {
|
||
if (statusMsgIdx < statusMessages.length) {
|
||
garatujaLoaderText.textContent = statusMessages[statusMsgIdx];
|
||
statusMsgIdx++;
|
||
} else {
|
||
garatujaLoaderText.textContent = 'Formatando parecer...';
|
||
}
|
||
}, 4500);
|
||
|
||
try {
|
||
const response = await fetch('/api/garatujas/analyze', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
clearInterval(statusInterval);
|
||
|
||
if (!response.ok) {
|
||
const errData = await response.json();
|
||
throw new Error(errData.error || 'Erro desconhecido');
|
||
}
|
||
|
||
const data = await response.json();
|
||
activeGaratujaReport = data;
|
||
|
||
if (window.marked) {
|
||
garatujaReportArea.innerHTML = marked.parse(data.analise);
|
||
} else {
|
||
garatujaReportArea.textContent = data.analise;
|
||
}
|
||
|
||
garatujaLoader.style.display = 'none';
|
||
garatujaResultArea.style.display = 'flex';
|
||
btnGenerateGaratuja.disabled = false;
|
||
|
||
} catch (err) {
|
||
clearInterval(statusInterval);
|
||
console.error(err);
|
||
alert('Erro na análise: ' + err.message);
|
||
garatujaLoader.style.display = 'none';
|
||
btnGenerateGaratuja.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Copiar Parecer Garatujas
|
||
if (btnCopyGaratujaReport) {
|
||
btnCopyGaratujaReport.addEventListener('click', () => {
|
||
if (activeGaratujaReport && activeGaratujaReport.analise) {
|
||
navigator.clipboard.writeText(activeGaratujaReport.analise)
|
||
.then(() => {
|
||
btnCopyGaratujaReport.textContent = '✅ Copiado!';
|
||
setTimeout(() => { btnCopyGaratujaReport.textContent = '📋 Copiar'; }, 2000);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// Baixar PDF Garatujas
|
||
if (btnPdfGaratujaReport) {
|
||
btnPdfGaratujaReport.addEventListener('click', () => {
|
||
if (!activeGaratujaReport) return;
|
||
const printWindow = window.open('', '_blank');
|
||
printWindow.document.write(`
|
||
<html>
|
||
<head>
|
||
<title>Garatujas - Laudo do Desenho</title>
|
||
<style>
|
||
body { font-family: 'Outfit', 'Inter', sans-serif; padding: 40px; color: #333; line-height: 1.6; }
|
||
h1, h2, h3 { color: #ea580c; margin-top: 24px; margin-bottom: 12px; }
|
||
h1 { border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; font-size: 22px; }
|
||
.meta-box { background: #f8fafc; padding: 16px; border-radius: 8px; margin-bottom: 24px; border: 1px solid #e2e8f0; display: flex; gap: 16px; align-items: center; }
|
||
.meta-label { font-weight: bold; color: #475569; }
|
||
img { border-radius: 6px; border: 1px solid #ddd; max-height: 120px; }
|
||
@media print {
|
||
body { padding: 0; }
|
||
button { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>🎨 Garatujas - Parecer de Traçado Infantil</h1>
|
||
<div class="meta-box">
|
||
<img src="${activeGaratujaReport.imageUrl}">
|
||
<div>
|
||
<div><span class="meta-label">Aluno(a):</span> <span>${activeGaratujaReport.alunoName}</span></div>
|
||
<div><span class="meta-label">Data de Análise:</span> <span>${new Date(activeGaratujaReport.createdAt).toLocaleDateString('pt-BR')}</span></div>
|
||
</div>
|
||
</div>
|
||
<div>${window.marked ? marked.parse(activeGaratujaReport.analise) : activeGaratujaReport.analise}</div>
|
||
<script>
|
||
window.onload = function() {
|
||
window.print();
|
||
setTimeout(() => { window.close(); }, 500);
|
||
};
|
||
<\/script>
|
||
</body>
|
||
</html>
|
||
`);
|
||
printWindow.document.close();
|
||
});
|
||
}
|
||
|
||
// Carregar Histórico Garatujas
|
||
async function loadGaratujasHistory() {
|
||
try {
|
||
const response = await fetch('/api/garatujas/list');
|
||
if (!response.ok) throw new Error('Erro ao carregar histórico');
|
||
const list = await response.json();
|
||
|
||
garatujasListContainer.innerHTML = '';
|
||
if (list.length === 0) {
|
||
garatujasNoData.style.display = 'block';
|
||
return;
|
||
}
|
||
garatujasNoData.style.display = 'none';
|
||
|
||
list.forEach(item => {
|
||
const div = document.createElement('div');
|
||
div.className = 'quick-note-card';
|
||
div.style.cursor = 'pointer';
|
||
div.style.display = 'flex';
|
||
div.style.flexDirection = 'column';
|
||
div.style.gap = '8px';
|
||
div.style.padding = '12px';
|
||
|
||
div.innerHTML = `
|
||
<div style="position: relative; width: 100%; aspect-ratio: 4/3; overflow: hidden; border-radius: 6px; background: rgba(0,0,0,0.2);">
|
||
<img src="${item.imageUrl}" style="width: 100%; height: 100%; object-fit: cover;">
|
||
<button class="delete-btn" data-id="${item.id}" style="position: absolute; top: 6px; right: 6px; background: rgba(0,0,0,0.5); border: none; width: 26px; height: 26px; border-radius: 50%; color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 0.8rem; transition: background 0.2s, transform 0.2s; z-index: 10;" title="Deletar análise" onmouseover="this.style.background='rgba(220,53,69,0.95)'; this.style.transform='scale(1.1)';" onmouseout="this.style.background='rgba(0,0,0,0.5)'; this.style.transform='scale(1)';">
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
<div style="font-weight: 600; color: var(--text-primary); font-size: 0.9rem;">${escapeHtml(item.alunoName)}</div>
|
||
<div style="font-size: 0.75rem; color: var(--text-muted);">${new Date(item.createdAt).toLocaleDateString('pt-BR')}</div>
|
||
`;
|
||
|
||
const deleteBtn = div.querySelector('.delete-btn');
|
||
deleteBtn.addEventListener('click', async (e) => {
|
||
e.stopPropagation();
|
||
if (confirm(`Deseja realmente excluir a análise de desenho do aluno "${item.alunoName}"?`)) {
|
||
try {
|
||
const delResp = await fetch(`/api/garatujas/${item.id}`, { method: 'DELETE' });
|
||
if (!delResp.ok) throw new Error('Falha ao deletar');
|
||
alert('Análise deletada com sucesso.');
|
||
loadGaratujasHistory();
|
||
if (activeGaratujaReport && activeGaratujaReport.id === item.id) {
|
||
activeGaratujaReport = null;
|
||
garatujasAlunoInput.value = '';
|
||
garatujaImgPreview.src = '';
|
||
garatujaPreviewContainer.style.display = 'none';
|
||
garatujaReportArea.innerHTML = '';
|
||
garatujaResultArea.style.display = 'none';
|
||
}
|
||
} catch (err) {
|
||
alert('Erro ao excluir análise: ' + err.message);
|
||
}
|
||
}
|
||
});
|
||
|
||
div.addEventListener('click', async () => {
|
||
try {
|
||
const detailResp = await fetch(`/api/garatujas/${item.id}`);
|
||
if (!detailResp.ok) throw new Error('Erro ao obter detalhes');
|
||
const detail = await detailResp.json();
|
||
|
||
activeGaratujaReport = detail;
|
||
garatujasAlunoInput.value = detail.alunoName;
|
||
garatujaImgPreview.src = detail.imageUrl;
|
||
garatujaPreviewContainer.style.display = 'block';
|
||
garatujaReportArea.innerHTML = window.marked ? marked.parse(detail.analise) : detail.analise;
|
||
|
||
garatujaResultArea.style.display = 'flex';
|
||
if (tabGaratujasNova) tabGaratujasNova.click();
|
||
} catch (e) {
|
||
alert('Falha ao abrir desenho.');
|
||
}
|
||
});
|
||
|
||
garatujasListContainer.appendChild(div);
|
||
});
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// ESTÚDIO DE POESIA
|
||
// ============================================================
|
||
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 btnGeneratePoesia = document.getElementById('btnGeneratePoesia');
|
||
const poesiaGenerateLoader = document.getElementById('poesiaGenerateLoader');
|
||
const poesiaResultArea = document.getElementById('poesiaResultArea');
|
||
const poesiaTextOutput = document.getElementById('poesiaTextOutput');
|
||
const btnCopyPoesia = document.getElementById('btnCopyPoesia');
|
||
const btnDownloadPoesiaPdf = document.getElementById('btnDownloadPoesiaPdf');
|
||
|
||
const poesiaTemaInput = document.getElementById('poesiaTemaInput');
|
||
const poesiaFaixaEtaria = document.getElementById('poesiaFaixaEtaria');
|
||
|
||
let currentPoesia = null;
|
||
let selectedEstrofes = 3;
|
||
|
||
// Seleção 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');
|
||
selectedEstrofes = parseInt(e.target.dataset.estrofes);
|
||
});
|
||
});
|
||
|
||
// Abrir/Fechar Modal Principal
|
||
if (barBtnPoesia) {
|
||
barBtnPoesia.addEventListener('click', () => {
|
||
poesiaModal.style.display = 'flex';
|
||
poesiaTemaInput.value = '';
|
||
poesiaResultArea.style.display = 'none';
|
||
poesiaGenerateLoader.style.display = 'none';
|
||
btnGeneratePoesia.disabled = false;
|
||
currentPoesia = null;
|
||
});
|
||
}
|
||
|
||
if (btnClosePoesiaModal) {
|
||
btnClosePoesiaModal.addEventListener('click', () => {
|
||
poesiaModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
// Abrir/Fechar Modal de Histórico
|
||
if (btnOpenPoesiaHistory) {
|
||
btnOpenPoesiaHistory.addEventListener('click', () => {
|
||
poesiaHistoryModal.style.display = 'flex';
|
||
loadPoesiaHistory();
|
||
});
|
||
}
|
||
|
||
if (btnClosePoesiaHistory) {
|
||
btnClosePoesiaHistory.addEventListener('click', () => {
|
||
poesiaHistoryModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
window.addEventListener('click', (e) => {
|
||
if (e.target === poesiaModal) poesiaModal.style.display = 'none';
|
||
if (e.target === poesiaHistoryModal) poesiaHistoryModal.style.display = 'none';
|
||
});
|
||
|
||
// Gerar Poesia
|
||
if (btnGeneratePoesia) {
|
||
btnGeneratePoesia.addEventListener('click', async () => {
|
||
const tema = poesiaTemaInput.value.trim();
|
||
const faixaEtaria = poesiaFaixaEtaria.value;
|
||
|
||
if (!tema) {
|
||
showToast('Por favor, digite o tema da poesia.', 'error');
|
||
return;
|
||
}
|
||
|
||
btnGeneratePoesia.disabled = true;
|
||
poesiaGenerateLoader.style.display = 'flex';
|
||
poesiaResultArea.style.display = 'none';
|
||
|
||
try {
|
||
const response = await fetch('/api/poesias/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ faixaEtaria, estrofes: selectedEstrofes, tema })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok && data.success) {
|
||
currentPoesia = data.poesia;
|
||
poesiaTextOutput.innerHTML = window.marked ? marked.parse(currentPoesia) : currentPoesia;
|
||
poesiaResultArea.style.display = 'flex';
|
||
showToast('Poesia criada com sucesso!', 'success');
|
||
} else {
|
||
showToast(data.error || 'Falha ao gerar poesia.', 'error');
|
||
}
|
||
} catch (err) {
|
||
console.error('Erro na geração da poesia:', err);
|
||
showToast('Erro de conexão ao gerar poesia.', 'error');
|
||
} finally {
|
||
btnGeneratePoesia.disabled = false;
|
||
poesiaGenerateLoader.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Copiar e Baixar
|
||
if (btnCopyPoesia) {
|
||
btnCopyPoesia.addEventListener('click', () => {
|
||
if (currentPoesia) {
|
||
navigator.clipboard.writeText(currentPoesia);
|
||
showToast('Poesia copiada para a área de transferência!', 'success');
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnDownloadPoesiaPdf) {
|
||
btnDownloadPoesiaPdf.addEventListener('click', () => {
|
||
if (!currentPoesia) return;
|
||
const printWindow = window.open('', '_blank');
|
||
printWindow.document.write(`
|
||
<html>
|
||
<head>
|
||
<title>Poesia - Assistente PedaGog</title>
|
||
<style>
|
||
body { font-family: 'Outfit', 'Inter', sans-serif; padding: 40px; color: #333; line-height: 1.6; }
|
||
h1, h2, h3 { color: #10b981; margin-top: 24px; margin-bottom: 12px; }
|
||
@media print {
|
||
body { padding: 0; }
|
||
button { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div style="max-width: 800px; margin: 0 auto;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; margin-bottom: 20px;">
|
||
<h1 style="margin: 0; border: none; padding: 0;">📜 Poesia Pedagógica</h1>
|
||
<button onclick="window.print()" style="background: #10b981; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: bold;">Imprimir / Salvar PDF</button>
|
||
</div>
|
||
<div>
|
||
${window.marked ? marked.parse(currentPoesia) : currentPoesia}
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`);
|
||
printWindow.document.close();
|
||
});
|
||
}
|
||
|
||
// Carregar Histórico
|
||
async function loadPoesiaHistory() {
|
||
const listContainer = document.getElementById('poesiaHistoryList');
|
||
if (!listContainer) return;
|
||
|
||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando...</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.02); padding: 14px; border-radius: 8px; border: 1px solid var(--border-light); display: flex; flex-direction: column; gap: 8px; cursor: pointer; position: relative;';
|
||
div.innerHTML = `
|
||
<div style="display: flex; justify-content: space-between; align-items: flex-start; padding-right: 30px;">
|
||
<strong style="color: var(--brand-green); font-size: 0.95rem;">${item.faixa_etaria} (${item.estrofes} Estrofes)</strong>
|
||
<span style="font-size: 0.75rem; color: var(--text-secondary);">${date}</span>
|
||
</div>
|
||
<p style="margin: 0; font-size: 0.85rem; color: var(--text-primary); opacity: 0.9;">Tema: ${item.tema}</p>
|
||
<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?')) {
|
||
try {
|
||
const delRes = await fetch(`/api/poesias/${item.id}`, { method: 'DELETE' });
|
||
if (delRes.ok) {
|
||
showToast('Poesia excluída.', 'success');
|
||
loadPoesiaHistory();
|
||
} else {
|
||
showToast('Erro ao excluir.', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Erro de conexão.', 'error');
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
try {
|
||
const res = await fetch(`/api/poesias/${item.id}`);
|
||
const detail = await res.json();
|
||
if (res.ok) {
|
||
currentPoesia = detail.poesia;
|
||
poesiaTextOutput.innerHTML = window.marked ? marked.parse(currentPoesia) : currentPoesia;
|
||
poesiaResultArea.style.display = 'flex';
|
||
poesiaHistoryModal.style.display = 'none';
|
||
}
|
||
} catch (err) {
|
||
showToast('Erro ao carregar poesia.', 'error');
|
||
}
|
||
});
|
||
|
||
listContainer.appendChild(div);
|
||
});
|
||
} else {
|
||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Nenhuma poesia salva ainda.</p>';
|
||
}
|
||
} catch (err) {
|
||
listContainer.innerHTML = '<p style="text-align:center; color:#ef4444;">Erro ao carregar histórico.</p>';
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// BRINCARATIVO (CIRCUITOS PSICOMOTORES E BRINCADEIRAS)
|
||
// ============================================================
|
||
const brincarAtivoModal = document.getElementById('brincarAtivoModal');
|
||
const btnCloseBrincarAtivoModal = document.getElementById('btnCloseBrincarAtivoModal');
|
||
const barBtnBrincarAtivo = document.getElementById('barBtnBrincarAtivo');
|
||
|
||
const tabBrincarAtivoNova = document.getElementById('tabBrincarAtivoNova');
|
||
const tabBrincarAtivoHistorico = document.getElementById('tabBrincarAtivoHistorico');
|
||
const contentBrincarAtivoNova = document.getElementById('contentBrincarAtivoNova');
|
||
const contentBrincarAtivoHistorico = document.getElementById('contentBrincarAtivoHistorico');
|
||
|
||
const brincarAtivoFaixaSelect = document.getElementById('brincarAtivoFaixaSelect');
|
||
const brincarAtivoRecursosInput = document.getElementById('brincarAtivoRecursosInput');
|
||
const btnGenerateBrincarAtivo = document.getElementById('btnGenerateBrincarAtivo');
|
||
const brincarAtivoLoader = document.getElementById('brincarAtivoLoader');
|
||
const brincarAtivoLoaderText = document.getElementById('brincarAtivoLoaderText');
|
||
const brincarAtivoResultArea = document.getElementById('brincarAtivoResultArea');
|
||
const brincarAtivoReportArea = document.getElementById('brincarAtivoReportArea');
|
||
|
||
const btnCopyBrincarAtivoReport = document.getElementById('btnCopyBrincarAtivoReport');
|
||
const btnPdfBrincarAtivoReport = document.getElementById('btnPdfBrincarAtivoReport');
|
||
const brincarAtivoListContainer = document.getElementById('brincarAtivoListContainer');
|
||
const brincarAtivoNoData = document.getElementById('brincarAtivoNoData');
|
||
|
||
let activeBrincarAtivoReport = null;
|
||
|
||
// Abas do BrincarAtivo
|
||
if (tabBrincarAtivoNova) {
|
||
tabBrincarAtivoNova.addEventListener('click', () => {
|
||
tabBrincarAtivoNova.classList.add('active');
|
||
tabBrincarAtivoHistorico.classList.remove('active');
|
||
contentBrincarAtivoNova.style.display = 'block';
|
||
contentBrincarAtivoHistorico.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
if (tabBrincarAtivoHistorico) {
|
||
tabBrincarAtivoHistorico.addEventListener('click', () => {
|
||
tabBrincarAtivoHistorico.classList.add('active');
|
||
tabBrincarAtivoNova.classList.remove('active');
|
||
contentBrincarAtivoNova.style.display = 'none';
|
||
contentBrincarAtivoHistorico.style.display = 'block';
|
||
loadBrincarAtivoHistory();
|
||
});
|
||
}
|
||
|
||
// Abrir Modal
|
||
if (barBtnBrincarAtivo) {
|
||
barBtnBrincarAtivo.addEventListener('click', () => {
|
||
brincarAtivoModal.style.display = 'flex';
|
||
brincarAtivoRecursosInput.value = '';
|
||
brincarAtivoResultArea.style.display = 'none';
|
||
brincarAtivoLoader.style.display = 'none';
|
||
btnGenerateBrincarAtivo.disabled = false;
|
||
activeBrincarAtivoReport = null;
|
||
if (tabBrincarAtivoNova) tabBrincarAtivoNova.click();
|
||
});
|
||
}
|
||
|
||
// Fechar Modal
|
||
if (btnCloseBrincarAtivoModal) {
|
||
btnCloseBrincarAtivoModal.addEventListener('click', () => {
|
||
brincarAtivoModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
window.addEventListener('click', (e) => {
|
||
if (e.target === brincarAtivoModal) {
|
||
brincarAtivoModal.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// Gerar Atividade
|
||
if (btnGenerateBrincarAtivo) {
|
||
btnGenerateBrincarAtivo.addEventListener('click', async () => {
|
||
const faixaEtaria = brincarAtivoFaixaSelect.value;
|
||
const recursos = brincarAtivoRecursosInput.value.trim();
|
||
|
||
if (!recursos) {
|
||
alert('Por favor, informe os materiais ou recursos disponíveis.');
|
||
return;
|
||
}
|
||
|
||
btnGenerateBrincarAtivo.disabled = true;
|
||
brincarAtivoLoader.style.display = 'flex';
|
||
brincarAtivoResultArea.style.display = 'none';
|
||
brincarAtivoLoaderText.textContent = 'Acessando referências BNCC...';
|
||
|
||
let statusMsgIdx = 0;
|
||
const statusMessages = [
|
||
'Analisando faixa etária psicomotora...',
|
||
'Estruturando proposta lúdica de circuito...',
|
||
'Conectando objetivos específicos da BNCC...',
|
||
'Gerando instruções passo a passo para o professor...',
|
||
'Finalizando plano de circuito ativo...'
|
||
];
|
||
|
||
const statusInterval = setInterval(() => {
|
||
if (statusMsgIdx < statusMessages.length) {
|
||
brincarAtivoLoaderText.textContent = statusMessages[statusMsgIdx];
|
||
statusMsgIdx++;
|
||
} else {
|
||
brincarAtivoLoaderText.textContent = 'Formatando atividade...';
|
||
}
|
||
}, 4500);
|
||
|
||
try {
|
||
const response = await fetch('/api/brincar-ativo/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ faixaEtaria, recursos })
|
||
});
|
||
|
||
clearInterval(statusInterval);
|
||
|
||
if (!response.ok) {
|
||
const errData = await response.json();
|
||
throw new Error(errData.error || 'Erro desconhecido');
|
||
}
|
||
|
||
const data = await response.json();
|
||
activeBrincarAtivoReport = data;
|
||
|
||
if (window.marked) {
|
||
brincarAtivoReportArea.innerHTML = marked.parse(data.atividade);
|
||
} else {
|
||
brincarAtivoReportArea.textContent = data.atividade;
|
||
}
|
||
|
||
brincarAtivoLoader.style.display = 'none';
|
||
brincarAtivoResultArea.style.display = 'flex';
|
||
btnGenerateBrincarAtivo.disabled = false;
|
||
|
||
} catch (err) {
|
||
clearInterval(statusInterval);
|
||
console.error(err);
|
||
alert('Erro ao gerar atividade: ' + err.message);
|
||
brincarAtivoLoader.style.display = 'none';
|
||
btnGenerateBrincarAtivo.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Copiar Parecer BrincarAtivo
|
||
if (btnCopyBrincarAtivoReport) {
|
||
btnCopyBrincarAtivoReport.addEventListener('click', () => {
|
||
if (activeBrincarAtivoReport && activeBrincarAtivoReport.atividade) {
|
||
navigator.clipboard.writeText(activeBrincarAtivoReport.atividade)
|
||
.then(() => {
|
||
btnCopyBrincarAtivoReport.textContent = '✅ Copiado!';
|
||
setTimeout(() => { btnCopyBrincarAtivoReport.textContent = '📋 Copiar'; }, 2000);
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// Baixar PDF BrincarAtivo
|
||
if (btnPdfBrincarAtivoReport) {
|
||
btnPdfBrincarAtivoReport.addEventListener('click', () => {
|
||
if (!activeBrincarAtivoReport) return;
|
||
const printWindow = window.open('', '_blank');
|
||
printWindow.document.write(`
|
||
<html>
|
||
<head>
|
||
<title>BrincarAtivo - Roteiro de Circuito</title>
|
||
<style>
|
||
body { font-family: 'Outfit', 'Inter', sans-serif; padding: 40px; color: #333; line-height: 1.6; }
|
||
h1, h2, h3 { color: #10a37f; margin-top: 24px; margin-bottom: 12px; }
|
||
h1 { border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; font-size: 22px; }
|
||
.meta-box { background: #f8fafc; padding: 16px; border-radius: 8px; margin-bottom: 24px; border: 1px solid #e2e8f0; }
|
||
.meta-label { font-weight: bold; color: #475569; }
|
||
@media print {
|
||
body { padding: 0; }
|
||
button { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>🏃♂️ BrincarAtivo - Planejamento Psicomotor</h1>
|
||
<div class="meta-box">
|
||
<div><span class="meta-label">Faixa Etária:</span> <span>${activeBrincarAtivoReport.faixaEtaria}</span></div>
|
||
<div><span class="meta-label">Recursos:</span> <span>${activeBrincarAtivoReport.recursos}</span></div>
|
||
<div><span class="meta-label">Data de Criação:</span> <span>${new Date(activeBrincarAtivoReport.createdAt).toLocaleDateString('pt-BR')}</span></div>
|
||
</div>
|
||
<div>${window.marked ? marked.parse(activeBrincarAtivoReport.atividade) : activeBrincarAtivoReport.atividade}</div>
|
||
<script>
|
||
window.onload = function() {
|
||
window.print();
|
||
setTimeout(() => { window.close(); }, 500);
|
||
};
|
||
<\/script>
|
||
</body>
|
||
</html>
|
||
`);
|
||
printWindow.document.close();
|
||
});
|
||
}
|
||
|
||
// Carregar Histórico BrincarAtivo
|
||
async function loadBrincarAtivoHistory() {
|
||
try {
|
||
const response = await fetch('/api/brincar-ativo/list');
|
||
if (!response.ok) throw new Error('Erro ao carregar histórico');
|
||
const list = await response.json();
|
||
|
||
brincarAtivoListContainer.innerHTML = '';
|
||
if (list.length === 0) {
|
||
brincarAtivoNoData.style.display = 'block';
|
||
return;
|
||
}
|
||
brincarAtivoNoData.style.display = 'none';
|
||
|
||
list.forEach(item => {
|
||
const div = document.createElement('div');
|
||
div.className = 'quick-note-card';
|
||
div.style.cursor = 'pointer';
|
||
div.style.display = 'flex';
|
||
div.style.flexDirection = 'column';
|
||
div.style.gap = '6px';
|
||
div.style.padding = '12px';
|
||
|
||
const lines = item.recursos || '';
|
||
const previewText = lines.length > 60 ? lines.substring(0, 60) + '...' : lines;
|
||
|
||
div.innerHTML = `
|
||
<div style="font-weight: 600; color: var(--text-primary); font-size: 0.92rem; display: flex; align-items: center; justify-content: space-between; gap: 6px;">
|
||
<span>🏃♂️ ${escapeHtml(item.faixaEtaria.split(' ')[0])} (${item.faixaEtaria.includes('bem pequenas') ? 'Bem Pequenas' : (item.faixaEtaria.includes('pequenas') ? 'Pequenas' : 'Bebês')})</span>
|
||
<button class="delete-btn" data-id="${item.id}" style="background: transparent; border: none; color: var(--text-muted); cursor: pointer; font-size: 0.95rem; padding: 2px 6px; border-radius: 4px; transition: color 0.2s, background 0.2s; display: flex; align-items: center; justify-content: center; z-index: 10;" title="Deletar atividade" onmouseover="this.style.color='#ff4d4f'; this.style.background='rgba(255, 77, 79, 0.1)'" onmouseout="this.style.color='var(--text-muted)'; this.style.background='transparent'">
|
||
🗑️
|
||
</button>
|
||
</div>
|
||
<div style="font-size: 0.8rem; color: var(--text-secondary);">Recursos: ${escapeHtml(previewText)}</div>
|
||
<div style="font-size: 0.75rem; color: var(--text-muted);">${new Date(item.createdAt).toLocaleDateString('pt-BR')}</div>
|
||
`;
|
||
|
||
const deleteBtn = div.querySelector('.delete-btn');
|
||
deleteBtn.addEventListener('click', async (e) => {
|
||
e.stopPropagation();
|
||
if (confirm(`Deseja realmente excluir esta atividade psicomotora do histórico?`)) {
|
||
try {
|
||
const delResp = await fetch(`/api/brincar-ativo/${item.id}`, { method: 'DELETE' });
|
||
if (!delResp.ok) throw new Error('Falha ao deletar');
|
||
alert('Atividade deletada com sucesso.');
|
||
loadBrincarAtivoHistory();
|
||
if (activeBrincarAtivoReport && activeBrincarAtivoReport.id === item.id) {
|
||
activeBrincarAtivoReport = null;
|
||
brincarAtivoFaixaSelect.value = '';
|
||
brincarAtivoRecursosInput.value = '';
|
||
brincarAtivoReportArea.innerHTML = '';
|
||
brincarAtivoResultArea.style.display = 'none';
|
||
}
|
||
} catch (err) {
|
||
alert('Erro ao excluir atividade: ' + err.message);
|
||
}
|
||
}
|
||
});
|
||
|
||
div.addEventListener('click', async () => {
|
||
try {
|
||
const detailResp = await fetch(`/api/brincar-ativo/${item.id}`);
|
||
if (!detailResp.ok) throw new Error('Erro ao obter detalhes');
|
||
const detail = await detailResp.json();
|
||
|
||
activeBrincarAtivoReport = detail;
|
||
brincarAtivoFaixaSelect.value = detail.faixaEtaria;
|
||
brincarAtivoRecursosInput.value = detail.recursos;
|
||
brincarAtivoReportArea.innerHTML = window.marked ? marked.parse(detail.atividade) : detail.atividade;
|
||
|
||
brincarAtivoResultArea.style.display = 'flex';
|
||
if (tabBrincarAtivoNova) tabBrincarAtivoNova.click();
|
||
} catch (e) {
|
||
alert('Falha ao abrir atividade.');
|
||
}
|
||
});
|
||
|
||
brincarAtivoListContainer.appendChild(div);
|
||
});
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
}
|
||
};
|
||
|
||
// Player de áudio personalizado global para as mídias geradas
|
||
window.toggleCustomAudio = (btn) => {
|
||
const player = btn.closest('.custom-audio-player');
|
||
const audio = player.querySelector('audio');
|
||
const playIcon = btn.querySelector('.play-icon');
|
||
const pauseIcon = btn.querySelector('.pause-icon');
|
||
const progress = player.querySelector('.audio-progress');
|
||
const timeSpan = player.querySelector('.audio-time');
|
||
|
||
if (audio.paused) {
|
||
// Pausar outros players que possam estar ativos
|
||
document.querySelectorAll('.custom-audio-player audio').forEach(a => {
|
||
if (a !== audio && !a.paused) {
|
||
a.pause();
|
||
const otherBtn = a.closest('.custom-audio-player').querySelector('.audio-play-btn');
|
||
otherBtn.querySelector('.play-icon').classList.remove('hidden');
|
||
otherBtn.querySelector('.pause-icon').classList.add('hidden');
|
||
}
|
||
});
|
||
|
||
audio.play();
|
||
playIcon.classList.add('hidden');
|
||
pauseIcon.classList.remove('hidden');
|
||
} else {
|
||
audio.pause();
|
||
playIcon.classList.remove('hidden');
|
||
pauseIcon.classList.add('hidden');
|
||
}
|
||
|
||
// Atualizar progresso em tempo real
|
||
audio.ontimeupdate = () => {
|
||
if (audio.duration) {
|
||
const percent = (audio.currentTime / audio.duration) * 100;
|
||
progress.style.width = percent + '%';
|
||
|
||
const mins = Math.floor(audio.currentTime / 60);
|
||
const secs = Math.floor(audio.currentTime % 60).toString().padStart(2, '0');
|
||
timeSpan.textContent = `${mins}:${secs}`;
|
||
}
|
||
};
|
||
|
||
audio.onended = () => {
|
||
playIcon.classList.remove('hidden');
|
||
pauseIcon.classList.add('hidden');
|
||
progress.style.width = '0%';
|
||
timeSpan.textContent = '0:00';
|
||
};
|
||
};
|
||
// ============================================================
|
||
// MUSICANDO IDEIAS
|
||
// ============================================================
|
||
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 musicaFaixaEtaria = document.getElementById('musicaFaixaEtaria');
|
||
const musicaRitmo = document.getElementById('musicaRitmo');
|
||
const musicaTemaInput = document.getElementById('musicaTemaInput');
|
||
const btnGenerateMusica = document.getElementById('btnGenerateMusica');
|
||
|
||
const musicaGenerateLoader = document.getElementById('musicaGenerateLoader');
|
||
const musicaResultArea = document.getElementById('musicaResultArea');
|
||
const musicaTextOutput = document.getElementById('musicaTextOutput');
|
||
const btnCopyMusica = document.getElementById('btnCopyMusica');
|
||
const btnDownloadMusicaPdf = document.getElementById('btnDownloadMusicaPdf');
|
||
|
||
let currentMusicaEstrofes = 3;
|
||
let currentMusica = '';
|
||
|
||
document.querySelectorAll('.btn-musica-estrofes').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
document.querySelectorAll('.btn-musica-estrofes').forEach(b => b.classList.remove('active'));
|
||
e.target.classList.add('active');
|
||
currentMusicaEstrofes = parseInt(e.target.dataset.estrofes);
|
||
});
|
||
});
|
||
|
||
if (barBtnMusica) {
|
||
barBtnMusica.addEventListener('click', () => {
|
||
musicaModal.style.display = 'flex';
|
||
musicaTemaInput.value = '';
|
||
musicaResultArea.style.display = 'none';
|
||
musicaGenerateLoader.style.display = 'none';
|
||
btnGenerateMusica.disabled = false;
|
||
currentMusica = '';
|
||
});
|
||
}
|
||
|
||
if (btnCloseMusicaModal) musicaModal.addEventListener('click', (e) => { if(e.target === musicaModal) musicaModal.style.display = 'none'; });
|
||
if (btnCloseMusicaModal) btnCloseMusicaModal.addEventListener('click', () => musicaModal.style.display = 'none');
|
||
|
||
if (btnOpenMusicaHistory) {
|
||
btnOpenMusicaHistory.addEventListener('click', () => {
|
||
musicaHistoryModal.style.display = 'flex';
|
||
loadMusicaHistory();
|
||
});
|
||
}
|
||
if (btnCloseMusicaHistory) btnCloseMusicaHistory.addEventListener('click', () => musicaHistoryModal.style.display = 'none');
|
||
musicaHistoryModal.addEventListener('click', (e) => { if(e.target === musicaHistoryModal) musicaHistoryModal.style.display = 'none'; });
|
||
|
||
if (btnGenerateMusica) {
|
||
btnGenerateMusica.addEventListener('click', async () => {
|
||
const tema = musicaTemaInput.value.trim();
|
||
if (!tema) {
|
||
showToast('Por favor, informe o tema da música.', 'error');
|
||
return;
|
||
}
|
||
|
||
btnGenerateMusica.disabled = true;
|
||
musicaResultArea.style.display = 'none';
|
||
musicaGenerateLoader.style.display = 'flex';
|
||
|
||
try {
|
||
const response = await fetch('/api/musicas/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
faixaEtaria: musicaFaixaEtaria.value,
|
||
estrofes: currentMusicaEstrofes,
|
||
ritmo: musicaRitmo.value,
|
||
tema: tema
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok) {
|
||
currentMusica = data.musica;
|
||
musicaTextOutput.innerHTML = window.marked ? marked.parse(currentMusica) : currentMusica;
|
||
musicaResultArea.style.display = 'flex';
|
||
} else {
|
||
showToast(data.error || 'Erro ao gerar música.', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Erro de conexão.', 'error');
|
||
} finally {
|
||
musicaGenerateLoader.style.display = 'none';
|
||
btnGenerateMusica.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnCopyMusica) {
|
||
btnCopyMusica.addEventListener('click', () => {
|
||
if (currentMusica) {
|
||
navigator.clipboard.writeText(currentMusica);
|
||
showToast('Música copiada para a área de transferência!', 'success');
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnDownloadMusicaPdf) {
|
||
btnDownloadMusicaPdf.addEventListener('click', () => {
|
||
if (!currentMusica) return;
|
||
const printWindow = window.open('', '_blank');
|
||
printWindow.document.write(`
|
||
<html>
|
||
<head>
|
||
<title>Música - Assistente PedaGog</title>
|
||
<style>
|
||
body { font-family: 'Outfit', 'Inter', sans-serif; padding: 40px; color: #333; line-height: 1.6; }
|
||
h1, h2, h3 { color: #10b981; margin-top: 24px; margin-bottom: 12px; }
|
||
@media print { body { padding: 0; } button { display: none; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div style="max-width: 800px; margin: 0 auto;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; margin-bottom: 20px;">
|
||
<h1 style="margin: 0;">🎵 Música Pedagógica</h1>
|
||
<button onclick="window.print()" style="background: #10b981; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: bold;">Imprimir / PDF</button>
|
||
</div>
|
||
<div>${window.marked ? marked.parse(currentMusica) : currentMusica}</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`);
|
||
printWindow.document.close();
|
||
});
|
||
}
|
||
|
||
async function loadMusicaHistory() {
|
||
const listContainer = document.getElementById('musicaHistoryList');
|
||
if (!listContainer) return;
|
||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando...</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.02); padding: 14px; border-radius: 8px; border: 1px solid var(--border-light); display: flex; flex-direction: column; gap: 8px; cursor: pointer; position: relative;';
|
||
div.innerHTML = `
|
||
<div style="display: flex; justify-content: space-between; align-items: flex-start; padding-right: 30px;">
|
||
<strong style="color: var(--brand-green); font-size: 0.95rem;">${item.faixa_etaria} - ${item.ritmo}</strong>
|
||
<span style="font-size: 0.75rem; color: var(--text-secondary);">${date}</span>
|
||
</div>
|
||
<p style="margin: 0; font-size: 0.85rem; color: var(--text-primary); opacity: 0.9;">Tema: ${item.tema}</p>
|
||
<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?')) {
|
||
try {
|
||
const delRes = await fetch(`/api/musicas/${item.id}`, { method: 'DELETE' });
|
||
if (delRes.ok) { showToast('Música excluída.', 'success'); loadMusicaHistory(); }
|
||
} catch (err) { showToast('Erro ao excluir.', 'error'); }
|
||
}
|
||
return;
|
||
}
|
||
try {
|
||
const res = await fetch(`/api/musicas/${item.id}`);
|
||
const detail = await res.json();
|
||
if (res.ok) {
|
||
currentMusica = detail.musica;
|
||
musicaTextOutput.innerHTML = window.marked ? marked.parse(currentMusica) : currentMusica;
|
||
musicaResultArea.style.display = 'flex';
|
||
musicaHistoryModal.style.display = 'none';
|
||
}
|
||
} catch (err) { showToast('Erro ao carregar.', 'error'); }
|
||
});
|
||
listContainer.appendChild(div);
|
||
});
|
||
} else {
|
||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Nenhum histórico.</p>';
|
||
}
|
||
} catch (err) { listContainer.innerHTML = '<p style="text-align:center; color:#ef4444;">Erro ao carregar.</p>'; }
|
||
}
|
||
|
||
|
||
// ============================================================
|
||
// INVENTANDO HISTORIAS
|
||
// ============================================================
|
||
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 historiaFaixaEtaria = document.getElementById('historiaFaixaEtaria');
|
||
const historiaParagrafos = document.getElementById('historiaParagrafos');
|
||
const historiaPersonagens = document.getElementById('historiaPersonagens');
|
||
const historiaEstilo = document.getElementById('historiaEstilo');
|
||
const historiaTemaInput = document.getElementById('historiaTemaInput');
|
||
const btnGenerateHistoria = document.getElementById('btnGenerateHistoria');
|
||
|
||
const historiaGenerateLoader = document.getElementById('historiaGenerateLoader');
|
||
const historiaResultArea = document.getElementById('historiaResultArea');
|
||
const historiaTextOutput = document.getElementById('historiaTextOutput');
|
||
const btnCopyHistoria = document.getElementById('btnCopyHistoria');
|
||
const btnDownloadHistoriaPdf = document.getElementById('btnDownloadHistoriaPdf');
|
||
|
||
let currentHistoria = '';
|
||
|
||
if (barBtnHistoria) {
|
||
barBtnHistoria.addEventListener('click', () => {
|
||
historiaModal.style.display = 'flex';
|
||
historiaTemaInput.value = '';
|
||
historiaResultArea.style.display = 'none';
|
||
historiaGenerateLoader.style.display = 'none';
|
||
btnGenerateHistoria.disabled = false;
|
||
currentHistoria = '';
|
||
});
|
||
}
|
||
|
||
if (btnCloseHistoriaModal) historiaModal.addEventListener('click', (e) => { if(e.target === historiaModal) historiaModal.style.display = 'none'; });
|
||
if (btnCloseHistoriaModal) btnCloseHistoriaModal.addEventListener('click', () => historiaModal.style.display = 'none');
|
||
|
||
if (btnOpenHistoriaHistory) {
|
||
btnOpenHistoriaHistory.addEventListener('click', () => {
|
||
historiaHistoryModal.style.display = 'flex';
|
||
loadHistoriaHistory();
|
||
});
|
||
}
|
||
if (btnCloseHistoriaHistory) btnCloseHistoriaHistory.addEventListener('click', () => historiaHistoryModal.style.display = 'none');
|
||
historiaHistoryModal.addEventListener('click', (e) => { if(e.target === historiaHistoryModal) historiaHistoryModal.style.display = 'none'; });
|
||
|
||
if (btnGenerateHistoria) {
|
||
btnGenerateHistoria.addEventListener('click', async () => {
|
||
const tema = historiaTemaInput.value.trim();
|
||
if (!tema) {
|
||
showToast('Por favor, informe o tema da história.', 'error');
|
||
return;
|
||
}
|
||
|
||
btnGenerateHistoria.disabled = true;
|
||
historiaResultArea.style.display = 'none';
|
||
historiaGenerateLoader.style.display = 'flex';
|
||
|
||
try {
|
||
const response = await fetch('/api/historias/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
faixaEtaria: historiaFaixaEtaria.value,
|
||
paragrafos: historiaParagrafos.value,
|
||
personagens: historiaPersonagens.value,
|
||
estilo: historiaEstilo.value,
|
||
tema: tema
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok) {
|
||
currentHistoria = data.historia;
|
||
historiaTextOutput.innerHTML = window.marked ? marked.parse(currentHistoria) : currentHistoria;
|
||
historiaResultArea.style.display = 'flex';
|
||
} else {
|
||
showToast(data.error || 'Erro ao inventar história.', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Erro de conexão.', 'error');
|
||
} finally {
|
||
historiaGenerateLoader.style.display = 'none';
|
||
btnGenerateHistoria.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnCopyHistoria) {
|
||
btnCopyHistoria.addEventListener('click', () => {
|
||
if (currentHistoria) {
|
||
navigator.clipboard.writeText(currentHistoria);
|
||
showToast('História copiada para a área de transferência!', 'success');
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnDownloadHistoriaPdf) {
|
||
btnDownloadHistoriaPdf.addEventListener('click', () => {
|
||
if (!currentHistoria) return;
|
||
const printWindow = window.open('', '_blank');
|
||
printWindow.document.write(`
|
||
<html>
|
||
<head>
|
||
<title>História - Assistente PedaGog</title>
|
||
<style>
|
||
body { font-family: 'Outfit', 'Inter', sans-serif; padding: 40px; color: #333; line-height: 1.6; }
|
||
h1, h2, h3 { color: #10b981; margin-top: 24px; margin-bottom: 12px; }
|
||
@media print { body { padding: 0; } button { display: none; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div style="max-width: 800px; margin: 0 auto;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; margin-bottom: 20px;">
|
||
<h1 style="margin: 0;">✍️ História Pedagógica</h1>
|
||
<button onclick="window.print()" style="background: #10b981; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: bold;">Imprimir / PDF</button>
|
||
</div>
|
||
<div>${window.marked ? marked.parse(currentHistoria) : currentHistoria}</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`);
|
||
printWindow.document.close();
|
||
});
|
||
}
|
||
|
||
async function loadHistoriaHistory() {
|
||
const listContainer = document.getElementById('historiaHistoryList');
|
||
if (!listContainer) return;
|
||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando...</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: 8px; cursor: pointer; position: relative;';
|
||
div.innerHTML = `
|
||
<div style="display: flex; justify-content: space-between; align-items: flex-start; padding-right: 30px;">
|
||
<strong style="color: var(--brand-green); font-size: 0.95rem;">${item.faixa_etaria} (${item.estilo})</strong>
|
||
<span style="font-size: 0.75rem; color: var(--text-secondary);">${date}</span>
|
||
</div>
|
||
<p style="margin: 0; font-size: 0.85rem; color: var(--text-primary); opacity: 0.9;">Tema: ${item.tema}</p>
|
||
<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?')) {
|
||
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) {
|
||
currentHistoria = detail.historia;
|
||
historiaTextOutput.innerHTML = window.marked ? marked.parse(currentHistoria) : currentHistoria;
|
||
historiaResultArea.style.display = 'flex';
|
||
historiaHistoryModal.style.display = 'none';
|
||
}
|
||
} catch (err) { showToast('Erro ao carregar.', 'error'); }
|
||
});
|
||
listContainer.appendChild(div);
|
||
});
|
||
} else {
|
||
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Nenhum histórico.</p>';
|
||
}
|
||
} catch (err) { listContainer.innerHTML = '<p style="text-align:center; color:#ef4444;">Erro ao carregar.</p>'; }
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', initApp);
|
||
} else {
|
||
initApp();
|
||
}
|
||
// ==========================================================================
|
||
// PLANEJAMENTO MIND LAB
|
||
// ==========================================================================
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const barBtnMindLab = document.getElementById('barBtnMindLab');
|
||
const mindLabModal = document.getElementById('mindLabModal');
|
||
const btnCloseMindLabModal = document.getElementById('btnCloseMindLabModal');
|
||
const mindLabGamesGrid = document.getElementById('mindLabGamesGrid');
|
||
const mindLabOutroJogoInput = document.getElementById('mindLabOutroJogoInput');
|
||
const btnGenerateMindLab = document.getElementById('btnGenerateMindLab');
|
||
|
||
const mindLabJogos = {
|
||
infantil: [
|
||
"Gato e Rato", "Cães e Gatos", "Dominó de Cores", "Quebra-cabeças", "Jogos de Encaixe", "Castelo Lógico", "Safari", "Outro"
|
||
],
|
||
fund1_inicio: [
|
||
"Mancala", "Quoridor", "Hora do Rush", "Bloqueio", "Lig 4", "Pinguins Numa Fria", "Encruzilhada", "Lince", "Outro"
|
||
],
|
||
fund1_fim: [
|
||
"Abalone", "Quarto", "Octógono Fantástico", "Sudoku", "Damas", "Resta Um", "Xadrez Chinês", "Cilada", "Outro"
|
||
],
|
||
fund2: [
|
||
"Xadrez", "Mastermind (Senha)", "Abalone Avançado", "Go", "Hex", "Reversi (Othello)", "Blokus", "Outro"
|
||
]
|
||
};
|
||
|
||
function renderMindLabGames(faixa) {
|
||
mindLabGamesGrid.innerHTML = '';
|
||
const jogos = mindLabJogos[faixa] || [];
|
||
jogos.forEach((jogo, index) => {
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = `btn-music-option btn-mindlab-jogo ${index === 0 ? 'active' : ''}`;
|
||
btn.dataset.jogo = jogo;
|
||
btn.textContent = jogo === 'Outro' ? '✏️ Outro...' : `🎲 ${jogo}`;
|
||
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.btn-mindlab-jogo').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
if (jogo === 'Outro') {
|
||
mindLabOutroJogoInput.style.display = 'block';
|
||
mindLabOutroJogoInput.focus();
|
||
} else {
|
||
mindLabOutroJogoInput.style.display = 'none';
|
||
}
|
||
});
|
||
mindLabGamesGrid.appendChild(btn);
|
||
});
|
||
mindLabOutroJogoInput.style.display = 'none';
|
||
}
|
||
|
||
// Setup Faixa Etária
|
||
document.querySelectorAll('.btn-mindlab-faixa').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.btn-mindlab-faixa').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
renderMindLabGames(btn.dataset.faixa);
|
||
});
|
||
});
|
||
|
||
// Setup Método e Foco
|
||
document.querySelectorAll('.btn-mindlab-metodo').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.btn-mindlab-metodo').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.btn-mindlab-foco').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.btn-mindlab-foco').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
});
|
||
});
|
||
|
||
if (barBtnMindLab) {
|
||
barBtnMindLab.addEventListener('click', () => {
|
||
mindLabModal.style.display = 'flex';
|
||
renderMindLabGames('infantil');
|
||
document.getElementById('mindLabTemaLivreInput').value = '';
|
||
});
|
||
}
|
||
|
||
if (btnCloseMindLabModal) {
|
||
btnCloseMindLabModal.addEventListener('click', () => {
|
||
mindLabModal.style.display = 'none';
|
||
});
|
||
mindLabModal.addEventListener('click', (e) => {
|
||
if (e.target === mindLabModal) mindLabModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
if (btnGenerateMindLab) {
|
||
btnGenerateMindLab.addEventListener('click', async () => {
|
||
const activeFaixa = document.querySelector('.btn-mindlab-faixa.active');
|
||
const activeJogoBtn = document.querySelector('.btn-mindlab-jogo.active');
|
||
const activeMetodo = document.querySelector('.btn-mindlab-metodo.active');
|
||
const activeFoco = document.querySelector('.btn-mindlab-foco.active');
|
||
|
||
const faixaStr = activeFaixa ? activeFaixa.textContent.replace('🧸 ', '').replace('🎒 ', '').replace('📚 ', '').replace('🎓 ', '') : '';
|
||
let jogoStr = activeJogoBtn ? activeJogoBtn.dataset.jogo : '';
|
||
if (jogoStr === 'Outro') {
|
||
jogoStr = mindLabOutroJogoInput.value.trim() || 'Jogo não especificado';
|
||
}
|
||
|
||
const metodoStr = activeMetodo ? activeMetodo.dataset.metodo : '';
|
||
const focoStr = activeFoco ? activeFoco.dataset.foco : '';
|
||
const duracaoStr = document.getElementById('mindLabDuracaoSelect').value;
|
||
const temaLivre = document.getElementById('mindLabTemaLivreInput').value.trim();
|
||
|
||
const btnG = document.getElementById('btnGenerateMindLab');
|
||
const loader = document.getElementById('mindLabGenerateLoader');
|
||
const resultArea = document.getElementById('mindLabResultArea');
|
||
const output = document.getElementById('mindLabOutput');
|
||
|
||
btnG.style.display = 'none';
|
||
loader.style.display = 'flex';
|
||
resultArea.style.display = 'none';
|
||
|
||
try {
|
||
const res = await fetch('/api/mindlab/generate', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` },
|
||
body: JSON.stringify({ faixa_etaria: faixaStr, jogo: jogoStr, metodo: metodoStr, foco: focoStr, duracao: duracaoStr, tema: temaLivre })
|
||
});
|
||
const data = await res.json();
|
||
if (!res.ok) throw new Error(data.error || 'Erro na requisição');
|
||
|
||
output.innerHTML = typeof marked !== 'undefined' ? marked.parse(data.planejamento) : data.planejamento;
|
||
output.dataset.raw = data.planejamento;
|
||
resultArea.style.display = 'block';
|
||
btnG.style.display = 'flex';
|
||
btnG.innerHTML = '🧠 Criar Novo Planejamento';
|
||
} catch (err) {
|
||
alert('Erro ao gerar: ' + err.message);
|
||
btnG.style.display = 'flex';
|
||
} finally {
|
||
loader.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
const btnCopyMindLab = document.getElementById('btnCopyMindLab');
|
||
if (btnCopyMindLab) {
|
||
btnCopyMindLab.addEventListener('click', () => {
|
||
const output = document.getElementById('mindLabOutput');
|
||
if (output && output.dataset.raw) {
|
||
navigator.clipboard.writeText(output.dataset.raw);
|
||
btnCopyMindLab.innerHTML = '✅ Copiado!';
|
||
setTimeout(() => {
|
||
btnCopyMindLab.innerHTML = '📋 Copiar';
|
||
}, 2000);
|
||
}
|
||
});
|
||
}
|
||
|
||
const btnPdfMindLab = document.getElementById('btnPdfMindLab');
|
||
if (btnPdfMindLab) {
|
||
btnPdfMindLab.addEventListener('click', () => {
|
||
const output = document.getElementById('mindLabOutput');
|
||
if (output && output.dataset.raw && window.jspdf) {
|
||
const { jsPDF } = window.jspdf;
|
||
const doc = new jsPDF();
|
||
doc.setFontSize(12);
|
||
doc.setFont("helvetica", "normal");
|
||
const lines = doc.splitTextToSize(output.dataset.raw.replace(/[#*]/g, ''), 180);
|
||
doc.text(lines, 15, 20);
|
||
doc.save('Planejamento_Mind_Lab.pdf');
|
||
} else {
|
||
alert('Carregando biblioteca PDF ou nenhum conteúdo para salvar.');
|
||
}
|
||
});
|
||
}
|
||
|
||
const btnOpenMindLabHistory = document.getElementById('btnOpenMindLabHistory');
|
||
const mindLabHistoryModal = document.getElementById('mindLabHistoryModal');
|
||
const btnCloseMindLabHistory = document.getElementById('btnCloseMindLabHistory');
|
||
const mindLabHistoryList = document.getElementById('mindLabHistoryList');
|
||
|
||
if (btnOpenMindLabHistory) {
|
||
btnOpenMindLabHistory.addEventListener('click', async () => {
|
||
mindLabHistoryModal.style.display = 'flex';
|
||
mindLabHistoryList.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">Carregando histórico...</div>';
|
||
try {
|
||
const res = await fetch('/api/mindlab/list', {
|
||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||
});
|
||
const data = await res.json();
|
||
mindLabHistoryList.innerHTML = '';
|
||
if (data.length === 0) {
|
||
mindLabHistoryList.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">Nenhum planejamento salvo ainda.</div>';
|
||
return;
|
||
}
|
||
data.forEach(item => {
|
||
const div = document.createElement('div');
|
||
div.style.background = 'var(--bg-tertiary)';
|
||
div.style.padding = '12px 15px';
|
||
div.style.borderRadius = '8px';
|
||
div.style.display = 'flex';
|
||
div.style.justifyContent = 'space-between';
|
||
div.style.alignItems = 'center';
|
||
div.innerHTML = `
|
||
<div>
|
||
<div style="font-weight: 600; font-size: 0.95rem; color: var(--text-primary);">🎲 ${item.jogo} - ${item.faixa_etaria}</div>
|
||
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-top: 4px;">🎯 ${item.foco}</div>
|
||
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 4px;">${new Date(item.created_at).toLocaleString()}</div>
|
||
</div>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button class="btn-ver-mindlab btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.85rem; border-radius: 6px;">Ver</button>
|
||
<button class="btn-del-mindlab btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.85rem; border-radius: 6px; color: #ef4444; border-color: rgba(239, 68, 68, 0.3);">🗑️</button>
|
||
</div>
|
||
`;
|
||
mindLabHistoryList.appendChild(div);
|
||
});
|
||
|
||
document.querySelectorAll('.btn-ver-mindlab').forEach(btn => {
|
||
btn.addEventListener('click', async (e) => {
|
||
const id = e.currentTarget.dataset.id;
|
||
try {
|
||
const res = await fetch(`/api/mindlab/${id}`, {
|
||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||
});
|
||
const itemData = await res.json();
|
||
|
||
document.getElementById('mindLabOutput').innerHTML = typeof marked !== 'undefined' ? marked.parse(itemData.planejamento) : itemData.planejamento;
|
||
document.getElementById('mindLabOutput').dataset.raw = itemData.planejamento;
|
||
document.getElementById('mindLabResultArea').style.display = 'block';
|
||
|
||
mindLabHistoryModal.style.display = 'none';
|
||
document.getElementById('btnGenerateMindLab').style.display = 'flex';
|
||
} catch (err) { alert('Erro ao carregar planejamento.'); }
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.btn-del-mindlab').forEach(btn => {
|
||
btn.addEventListener('click', async (e) => {
|
||
if(!confirm('Tem certeza que deseja apagar este planejamento?')) return;
|
||
const id = e.currentTarget.dataset.id;
|
||
try {
|
||
await fetch(`/api/mindlab/${id}`, {
|
||
method: 'DELETE',
|
||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
|
||
});
|
||
e.currentTarget.closest('div[style*="var(--bg-tertiary)"]').remove();
|
||
} catch (err) { alert('Erro ao deletar.'); }
|
||
});
|
||
});
|
||
|
||
} catch (err) {
|
||
mindLabHistoryList.innerHTML = '<div style="color: #ef4444; padding: 20px;">Erro ao carregar histórico.</div>';
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnCloseMindLabHistory) {
|
||
btnCloseMindLabHistory.addEventListener('click', () => {
|
||
mindLabHistoryModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
// ============================================================
|
||
// CRIAR CARTAZES
|
||
// ============================================================
|
||
const barBtnCartazes = document.getElementById('barBtnCartazes');
|
||
const cartazesModal = document.getElementById('cartazesModal');
|
||
const btnCloseCartazesModal = document.getElementById('btnCloseCartazesModal');
|
||
|
||
const tabCartazesCriar = document.getElementById('tabCartazesCriar');
|
||
const tabCartazesHistorico = document.getElementById('tabCartazesHistorico');
|
||
const panelCartazesCriar = document.getElementById('panelCartazesCriar');
|
||
const panelCartazesHistorico = document.getElementById('panelCartazesHistorico');
|
||
|
||
const cartazTituloInput = document.getElementById('cartazTituloInput');
|
||
const cartazTemaInput = document.getElementById('cartazTemaInput');
|
||
const cartazLayoutSelect = document.getElementById('cartazLayoutSelect');
|
||
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 btnGenerateCartaz = document.getElementById('btnGenerateCartaz');
|
||
const cartazGenerateLoader = document.getElementById('cartazGenerateLoader');
|
||
|
||
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;
|
||
|
||
// Toggle Modal
|
||
if (barBtnCartazes) {
|
||
barBtnCartazes.addEventListener('click', () => {
|
||
cartazesModal.style.display = 'flex';
|
||
switchCartazTab('criar');
|
||
});
|
||
}
|
||
|
||
if (btnCloseCartazesModal) {
|
||
btnCloseCartazesModal.addEventListener('click', () => {
|
||
if (canvasModeActive) {
|
||
exitCanvasMode();
|
||
}
|
||
cartazesModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
cartazesModal.addEventListener('click', (e) => {
|
||
if (e.target === cartazesModal) {
|
||
if (canvasModeActive) {
|
||
exitCanvasMode();
|
||
}
|
||
cartazesModal.style.display = 'none';
|
||
}
|
||
});
|
||
|
||
// 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'));
|
||
|
||
// 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;
|
||
|
||
const tamanho = document.getElementById('cartazTamanhoSelect') ? document.getElementById('cartazTamanhoSelect').value : 'A4';
|
||
|
||
const FORMAT_SIZES = {
|
||
'A4': { retrato: 380, paisagem: 537 },
|
||
'A3': { retrato: 480, paisagem: 679 },
|
||
'A2': { retrato: 580, paisagem: 820 },
|
||
'A1': { retrato: 680, paisagem: 962 }
|
||
};
|
||
|
||
const maxW = FORMAT_SIZES[tamanho]?.[orientacao] || FORMAT_SIZES['A4']['retrato'];
|
||
|
||
cartazPrintableArea.style.width = '100%';
|
||
cartazPrintableArea.style.maxWidth = `${maxW}px`;
|
||
cartazPrintableArea.style.aspectRatio = orientacao === 'paisagem' ? '1.414 / 1' : '1 / 1.414';
|
||
cartazPrintableArea.style.height = 'auto';
|
||
|
||
if (orientacao === 'paisagem') {
|
||
cartazPreviewContent.style.flexDirection = 'row';
|
||
cartazPreviewContent.style.flexWrap = 'wrap';
|
||
cartazPreviewContent.style.justifyContent = 'center';
|
||
cartazPreviewContent.style.alignContent = 'flex-start';
|
||
cartazPreviewContent.style.gap = '12px';
|
||
|
||
setTimeout(() => {
|
||
Array.from(cartazPreviewContent.children).forEach(child => {
|
||
child.style.flex = '1 1 calc(50% - 12px)';
|
||
child.style.boxSizing = 'border-box';
|
||
child.style.margin = '0';
|
||
child.style.background = 'rgba(255,255,255,0.25)';
|
||
child.style.padding = '12px';
|
||
child.style.borderRadius = '8px';
|
||
child.style.border = '1px solid rgba(0,0,0,0.05)';
|
||
});
|
||
}, 0);
|
||
} else {
|
||
cartazPreviewContent.style.flexDirection = 'column';
|
||
cartazPreviewContent.style.flexWrap = 'nowrap';
|
||
cartazPreviewContent.style.justifyContent = 'flex-start';
|
||
cartazPreviewContent.style.gap = '12px';
|
||
|
||
setTimeout(() => {
|
||
Array.from(cartazPreviewContent.children).forEach(child => {
|
||
child.style.flex = '0 0 auto';
|
||
child.style.width = '100%';
|
||
child.style.boxSizing = 'border-box';
|
||
child.style.margin = '0';
|
||
child.style.background = 'rgba(255,255,255,0.25)';
|
||
child.style.padding = '12px';
|
||
child.style.borderRadius = '8px';
|
||
child.style.border = '1px solid rgba(0,0,0,0.05)';
|
||
});
|
||
}, 0);
|
||
}
|
||
|
||
cartazPreviewTitle.textContent = title;
|
||
cartazPreviewTitle.style.borderColor = text;
|
||
|
||
if (border === 'none') {
|
||
cartazPrintableArea.style.border = 'none';
|
||
} else {
|
||
cartazPrintableArea.style.border = `${border} ${text}`;
|
||
}
|
||
}
|
||
|
||
[
|
||
cartazBgColor,
|
||
cartazTextColor,
|
||
cartazFontSelect,
|
||
cartazBorderSelect,
|
||
cartazTituloInput,
|
||
document.getElementById('cartazOrientacaoSelect'),
|
||
document.getElementById('cartazTamanhoSelect')
|
||
].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 = e.target.dataset.bg;
|
||
const text = e.target.dataset.text;
|
||
cartazBgColor.value = bg;
|
||
cartazTextColor.value = text;
|
||
updateLivePreview();
|
||
});
|
||
});
|
||
|
||
// Toggle HTML Editor
|
||
if (btnEditCartazToggle) {
|
||
btnEditCartazToggle.addEventListener('click', () => {
|
||
if (cartazEditArea.style.display === 'none') {
|
||
cartazEditArea.style.display = 'flex';
|
||
btnEditCartazToggle.textContent = '👁️ Ocultar Editor';
|
||
} else {
|
||
cartazEditArea.style.display = 'none';
|
||
btnEditCartazToggle.textContent = '✏️ Editar Conteúdo';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Update from Editor
|
||
if (btnUpdateCartazPreview) {
|
||
btnUpdateCartazPreview.addEventListener('click', () => {
|
||
cartazPreviewContent.innerHTML = cartazHtmlEditor.value;
|
||
});
|
||
}
|
||
|
||
// Generate Poster
|
||
if (btnGenerateCartaz) {
|
||
btnGenerateCartaz.addEventListener('click', async () => {
|
||
const titulo = cartazTituloInput.value.trim();
|
||
const tema = cartazTemaInput.value.trim();
|
||
const layout = cartazLayoutSelect.value;
|
||
const promptImagem = cartazPromptImagem.value.trim();
|
||
const tamanho = document.getElementById('cartazTamanhoSelect') ? document.getElementById('cartazTamanhoSelect').value : 'A4';
|
||
const orientacao = document.getElementById('cartazOrientacaoSelect') ? document.getElementById('cartazOrientacaoSelect').value : 'retrato';
|
||
|
||
if (!titulo || !tema) {
|
||
showToast('Por favor, defina o Título e o Tema do Cartaz.', 'error');
|
||
return;
|
||
}
|
||
|
||
btnGenerateCartaz.disabled = true;
|
||
cartazGenerateLoader.style.display = 'flex';
|
||
|
||
try {
|
||
const res = await fetch('/api/cartazes/generate', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||
},
|
||
body: JSON.stringify({
|
||
titulo,
|
||
tema,
|
||
corFundo: cartazBgColor.value,
|
||
corTexto: cartazTextColor.value,
|
||
layout,
|
||
promptImagem,
|
||
tamanho,
|
||
orientacao
|
||
})
|
||
});
|
||
|
||
const data = await res.json();
|
||
if (res.ok) {
|
||
activeCartazData = data;
|
||
|
||
// Preencher Preview
|
||
cartazPreviewContent.innerHTML = data.conteudo;
|
||
cartazHtmlEditor.value = data.conteudo;
|
||
|
||
if (data.imagemUrl) {
|
||
cartazPreviewImage.src = data.imagemUrl;
|
||
cartazPreviewImageContainer.style.display = 'block';
|
||
} else {
|
||
cartazPreviewImageContainer.style.display = 'none';
|
||
}
|
||
|
||
updateLivePreview();
|
||
showToast('Cartaz criado e salvo com sucesso!', 'success');
|
||
} else {
|
||
showToast(data.error || 'Erro ao gerar cartaz.', 'error');
|
||
}
|
||
} catch (err) {
|
||
console.error(err);
|
||
showToast('Erro ao se conectar com o servidor.', 'error');
|
||
} finally {
|
||
btnGenerateCartaz.disabled = false;
|
||
cartazGenerateLoader.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
// 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;
|
||
|
||
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();
|
||
|
||
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;
|
||
|
||
document.addEventListener('mousemove', handleCanvasResize);
|
||
document.addEventListener('mouseup', stopCanvasResize);
|
||
});
|
||
|
||
handle.addEventListener('touchstart', (e) => {
|
||
if (!canvasModeActive) return;
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
|
||
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.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.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();
|
||
}
|
||
});
|