7413 lines
296 KiB
JavaScript
7413 lines
296 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
|
||
// ============================================================
|
||
const minhaTurmaModal = document.getElementById('minhaTurmaModal');
|
||
const btnMinhaTurma = document.getElementById('btnMinhaTurma');
|
||
const btnCloseMinhaTurmaModal = document.getElementById('btnCloseMinhaTurmaModal');
|
||
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');
|
||
|
||
// Fields
|
||
const cFormId = document.getElementById('childFormId');
|
||
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');
|
||
|
||
// Fields Turma Config
|
||
const tabTurmaConfig = document.getElementById('tabTurmaConfig');
|
||
const tabTurmaAlunos = document.getElementById('tabTurmaAlunos');
|
||
const turmaConfigSection = document.getElementById('turmaConfigSection');
|
||
const turmaAlunosSection = document.getElementById('turmaAlunosSection');
|
||
|
||
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 btnSaveTurmaConfig = document.getElementById('btnSaveTurmaConfig');
|
||
|
||
// Tab 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';
|
||
});
|
||
}
|
||
|
||
// Toggle Especial Fields
|
||
if (cFormEspecial) {
|
||
cFormEspecial.addEventListener('change', () => {
|
||
if (cFormEspecial.checked) {
|
||
divEspecialDetalhes.style.display = 'flex';
|
||
} else {
|
||
divEspecialDetalhes.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
const loadTurma = () => {
|
||
return JSON.parse(localStorage.getItem('camila_turma') || '[]');
|
||
};
|
||
|
||
const saveTurma = (turma) => {
|
||
localStorage.setItem('camila_turma', JSON.stringify(turma));
|
||
};
|
||
|
||
const loadTurmaConfig = () => {
|
||
return JSON.parse(localStorage.getItem('camila_config_turma') || '{}');
|
||
};
|
||
|
||
const saveTurmaConfig = (cfg) => {
|
||
localStorage.setItem('camila_config_turma', JSON.stringify(cfg));
|
||
showCustomAlert('Sucesso', 'Configurações da turma salvas com sucesso!');
|
||
};
|
||
|
||
if (btnSaveTurmaConfig) {
|
||
btnSaveTurmaConfig.addEventListener('click', () => {
|
||
saveTurmaConfig({
|
||
nome: configTurmaNome.value,
|
||
sala: configTurmaSala.value,
|
||
periodo: configTurmaPeriodo.value,
|
||
profTitular: configTurmaProfTitular.value,
|
||
profAux1: configTurmaProfAux1.value,
|
||
profAux2: configTurmaProfAux2.value,
|
||
cuidadora: configTurmaCuidadora.value
|
||
});
|
||
});
|
||
}
|
||
|
||
const populateTurmaConfig = () => {
|
||
const cfg = loadTurmaConfig();
|
||
if(configTurmaNome) configTurmaNome.value = cfg.nome || '';
|
||
if(configTurmaSala) configTurmaSala.value = cfg.sala || '';
|
||
if(configTurmaPeriodo) configTurmaPeriodo.value = cfg.periodo || 'Integral';
|
||
if(configTurmaProfTitular) configTurmaProfTitular.value = cfg.profTitular || '';
|
||
if(configTurmaProfAux1) configTurmaProfAux1.value = cfg.profAux1 || '';
|
||
if(configTurmaProfAux2) configTurmaProfAux2.value = cfg.profAux2 || '';
|
||
if(configTurmaCuidadora) configTurmaCuidadora.value = cfg.cuidadora || '';
|
||
};
|
||
|
||
const renderTurmaList = () => {
|
||
if (!childrenList) return;
|
||
const turma = loadTurma();
|
||
if (turma.length === 0) {
|
||
childrenList.innerHTML = '<div style="color: var(--text-secondary); text-align: center; padding: 20px;">Nenhuma criança cadastrada.</div>';
|
||
return;
|
||
}
|
||
|
||
// Sort by name
|
||
turma.sort((a, b) => (a.nome || '').localeCompare(b.nome || ''));
|
||
|
||
childrenList.innerHTML = turma.map(c => `
|
||
<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(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('');
|
||
};
|
||
|
||
const clearChildForm = () => {
|
||
cFormId.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';
|
||
};
|
||
|
||
window.editChild = (id) => {
|
||
const turma = loadTurma();
|
||
const c = turma.find(x => x.id === id);
|
||
if (!c) return;
|
||
|
||
cFormId.value = c.id;
|
||
cFormNome.value = c.nome || '';
|
||
cFormApelido.value = c.apelido || '';
|
||
cFormDataNasc.value = c.dataNasc || '';
|
||
cFormEspecial.checked = !!c.especial;
|
||
cFormEspecialDetalhes.value = c.especialDetalhes || '';
|
||
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, apenas removerá da lista da turma.', true);
|
||
if (isOk) {
|
||
let turma = loadTurma();
|
||
turma = turma.filter(x => x.id !== id);
|
||
saveTurma(turma);
|
||
renderTurmaList();
|
||
clearChildForm();
|
||
}
|
||
};
|
||
|
||
if (btnMinhaTurma) {
|
||
btnMinhaTurma.addEventListener('click', () => {
|
||
minhaTurmaModal.style.display = 'flex';
|
||
populateTurmaConfig();
|
||
renderTurmaList();
|
||
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', () => {
|
||
if (!cFormNome.value.trim()) {
|
||
showCustomAlert('Aviso', 'O Nome Completo é obrigatório.');
|
||
return;
|
||
}
|
||
|
||
let turma = loadTurma();
|
||
const id = cFormId.value || 'child_' + Date.now();
|
||
|
||
const newChild = {
|
||
id,
|
||
nome: cFormNome.value.trim(),
|
||
apelido: cFormApelido.value.trim(),
|
||
dataNasc: cFormDataNasc.value,
|
||
especial: cFormEspecial.checked,
|
||
especialDetalhes: cFormEspecialDetalhes.value.trim(),
|
||
pais: cFormPais.value.trim(),
|
||
autorizados: cFormAutorizados.value.trim()
|
||
};
|
||
|
||
const idx = turma.findIndex(x => x.id === id);
|
||
if (idx > -1) {
|
||
turma[idx] = newChild;
|
||
} else {
|
||
turma.push(newChild);
|
||
}
|
||
|
||
saveTurma(turma);
|
||
renderTurmaList();
|
||
clearChildForm();
|
||
});
|
||
}
|
||
|
||
// 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", "dataNasc" (formato YYYY-MM-DD), "especial" (boolean), "especialDetalhes", "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;
|
||
// Extract JSON block
|
||
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.turma) cFormTurma.value = parsed.turma;
|
||
if (parsed.sala) cFormSala.value = parsed.sala;
|
||
if (parsed.periodo) cFormPeriodo.value = parsed.periodo;
|
||
if (parsed.auxiliares) cFormAuxiliares.value = parsed.auxiliares;
|
||
if (parsed.nome) cFormNome.value = parsed.nome;
|
||
if (parsed.apelido) cFormApelido.value = parsed.apelido;
|
||
if (parsed.dataNasc) cFormDataNasc.value = parsed.dataNasc;
|
||
if (parsed.especial) cFormEspecial.checked = parsed.especial;
|
||
divEspecialDetalhes.style.display = cFormEspecial.checked ? 'flex' : 'none';
|
||
if (parsed.especialDetalhes) cFormEspecialDetalhes.value = parsed.especialDetalhes;
|
||
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', () => {
|
||
cartazesModal.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
cartazesModal.addEventListener('click', (e) => {
|
||
if (e.target === cartazesModal) 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'
|
||
? `<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>`
|
||
: '';
|
||
|
||
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>
|
||
@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;
|
||
}
|
||
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;
|
||
}
|
||
@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">
|
||
<h1>${title}</h1>
|
||
${imgContainerHtml}
|
||
<div class="content-area">
|
||
${cartazPreviewContent.innerHTML}
|
||
</div>
|
||
</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>';
|
||
}
|
||
}
|
||
});
|