Files
camila/public/app.js
T

8043 lines
320 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// 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, '&quot;')}">
<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, '&quot;')}">
<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, '&quot;')}">
<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, '&quot;');
const actionsDiv = document.createElement('div');
actionsDiv.className = 'message-actions';
actionsDiv.innerHTML = `
<button class="msg-action-btn btn-copy-msg" title="Copiar resposta" data-content="${escapedContent}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" />
</svg>
<span>Copiar</span>
</button>
<button class="msg-action-btn btn-regenerate" title="Regenerar resposta">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
<span>Regenerar</span>
</button>
<button class="msg-action-btn btn-speak" title="Ouvir resposta" data-content="${escapedContent}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.114 5.636a9 9 0 0 1 0 12.728M16.463 8.288a5.25 5.25 0 0 1 0 7.424M6.75 8.25l4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z" />
</svg>
<span>Ouvir</span>
</button>
`;
const wrapper = botRow.querySelector('.message-content-wrapper');
wrapper.appendChild(actionsDiv);
// Vincular listeners dos botões
const copyBtn = actionsDiv.querySelector('.btn-copy-msg');
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(assistantContent).then(() => {
const span = copyBtn.querySelector('span');
const original = span.textContent;
span.textContent = 'Copiado!';
setTimeout(() => span.textContent = original, 2000);
});
});
const regenBtn = actionsDiv.querySelector('.btn-regenerate');
regenBtn.addEventListener('click', () => {
if (!isGenerating) regenerateLastResponse();
});
const speakBtn = actionsDiv.querySelector('.btn-speak');
speakBtn.addEventListener('click', () => {
speakText(assistantContent, speakBtn);
});
scrollToBottom();
// AUTO-TTS: Se a mensagem veio do microfone, ler a resposta em voz alta automaticamente
if (lastInputWasVoice && assistantContent && !isMediaResponse) {
console.log('[VOZ] Mensagem veio do microfone — respondendo por voz automaticamente.');
setTimeout(() => {
speakText(assistantContent, speakBtn);
}, 400);
}
lastInputWasVoice = false;
// Salvar resposta no estado e localStorage
const assistantMsg = { role: 'assistant', content: assistantContent };
chat.messages.push(assistantMsg);
chat.updatedAt = Date.now();
saveChats();
renderHistory();
} catch (error) {
console.error('Erro na resposta do chat:', error);
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
const cursor = botContentDiv.querySelector('.typing-cursor');
if (cursor) cursor.remove();
botContentDiv.innerHTML = `
<p style="color: var(--error); margin-top: 8px; font-size: 13.5px; display: flex; align-items: center; gap: 6px;">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" style="width: 16px; height: 16px;">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
</svg>
Desculpe, ocorreu um erro ao obter resposta da PedaGog. Por favor, tente novamente.
</p>
`;
scrollToBottom();
} finally {
isGenerating = false;
btnSend.disabled = userInput.value.trim() === '';
userInput.disabled = false;
userInput.focus();
}
};
// Envio pelo Formulário
chatForm.addEventListener('submit', (e) => {
e.preventDefault();
const text = userInput.value.trim();
handleSendMessage(text);
});
// Botões de Modos Rápidos de Prompt (Rodapé)
document.querySelectorAll('.btn-prompt-mode').forEach(btn => {
btn.addEventListener('click', () => {
if (window.innerWidth <= 768) {
if (promptModesBar) promptModesBar.classList.remove('show');
if (fabTools) fabTools.classList.remove('active');
}
if (btn.dataset.prompt) {
handleSendMessage(btn.dataset.prompt);
return;
}
if (btn.dataset.fill) {
userInput.value = btn.dataset.fill;
userInput.focus();
btnSend.disabled = false;
return;
}
if (btn.id === 'barBtnEstudio') return; // Manipulado separadamente
if (btn.id === 'barBtnComics') return; // Manipulado separadamente
if (btn.id === 'barBtnMusica') return;
if (btn.id === 'barBtnHistoria') return;
if (btn.id === 'barBtnMindLab') return;
const mode = btn.dataset.mode;
const text = userInput.value.trim();
let suggestion = "";
let baseText = text;
// Se o texto atual já for uma das sugestões padrão, limpamos para não duplicar
const suggestionsList = [
"Crie uma música pedagógica alegre e animada sobre ",
"Crie um roteiro detalhado para um vídeo educativo sobre ",
"Crie uma ilustração pedagógica com cores vibrantes mostrando ",
"Escreva uma poesia infantil rimada e educativa sobre ",
"Escreva uma história educativa infantil curta sobre "
];
for (const sugg of suggestionsList) {
if (baseText.startsWith(sugg)) {
baseText = baseText.replace(sugg, "").trim();
break;
}
}
if (mode === 'AUDIO') {
suggestion = "Crie uma música pedagógica alegre e animada sobre ";
} else if (mode === 'VIDEO') {
suggestion = "Crie um roteiro detalhado para um vídeo educativo sobre ";
} else if (mode === 'IMAGE') {
suggestion = "Crie uma ilustração pedagógica com cores vibrantes mostrando ";
} else if (mode === 'POETRY') {
suggestion = "Escreva uma poesia infantil rimada e educativa sobre ";
} else if (mode === 'TEXT') {
suggestion = "Escreva uma história educativa infantil curta sobre ";
}
userInput.value = suggestion + baseText;
userInput.focus();
// Mover cursor para o final do texto
userInput.selectionStart = userInput.selectionEnd = userInput.value.length;
// Ajustar altura do textarea para acomodar a sugestão
userInput.style.height = 'auto';
userInput.style.height = userInput.scrollHeight + 'px';
if (btnSend) btnSend.disabled = false;
});
});
// Sugestões de Prompt Rápidas
suggestionCards.forEach(card => {
card.addEventListener('click', () => {
const prompt = card.dataset.prompt;
if (prompt) {
handleSendMessage(prompt);
}
});
});
// ==========================================================================
// LOGOUT & INICIALIZAÇÃO
// ==========================================================================
btnLogout.addEventListener('click', async () => {
try {
const response = await fetch('/api/logout', {
method: 'POST'
});
if (response.ok) {
window.location.href = '/login';
}
} catch (error) {
console.error('Erro ao fazer logout:', error);
}
});
// Inicializar interface
if (currentChatId) {
// Tenta carregar o chat ativo
const activeChat = chats.find(c => c.id === currentChatId);
if (activeChat) {
selectChat(currentChatId);
} else {
createNewChat();
}
} else {
createNewChat();
}
// Inicializar reconhecimento de voz
initVoiceRecognition();
// ============================================================
// SISTEMA DE OBSERVAÇÃO POR VOZ
// ============================================================
const recordModal = document.getElementById('recordModal');
const fabRecord = document.getElementById('fabRecord');
const btnExitRecord = document.getElementById('btnExitRecord');
const btnStopRecord = document.getElementById('btnStopRecord');
const recordStateRecording = document.getElementById('recordStateRecording');
const recordStateProcessing = document.getElementById('recordStateProcessing');
const recordStatePreview = document.getElementById('recordStatePreview');
const recordTimer = document.getElementById('recordTimer');
const reportContent = document.getElementById('reportContent');
const previewDateTime = document.getElementById('previewDateTime');
const btnDiscardReport = document.getElementById('btnDiscardReport');
const btnSaveReport = document.getElementById('btnSaveReport');
// Novos elementos de transcrição
const recordStateTranscribed = document.getElementById('recordStateTranscribed');
const transcriptionTextarea = document.getElementById('transcriptionTextarea');
const btnDiscardTranscription = document.getElementById('btnDiscardTranscription');
const btnAnalyzeTranscription = document.getElementById('btnAnalyzeTranscription');
const recordProcessingText = document.getElementById('recordProcessingText');
const recordProcessingSubtext = document.getElementById('recordProcessingSubtext');
// Variáveis de gravação
let mediaRecorder = null;
let audioChunks = [];
let recordingStartTime = null;
let timerInterval = null;
let audioContext = null;
let analyser = null;
let frequencyData = null;
let silenceFrames = 0;
let totalFrames = 0;
let vadCheckInterval = null;
let generatedReport = null;
let generatedCriancas = null;
let generatedTurma = 'não informada';
let generatedTags = [];
// Fechar modal - colocada no topo para evitar problemas de inicialização
function closeRecordModal() {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
try {
mediaRecorder.stop();
} catch (e) {}
if (mediaRecorder.stream) {
mediaRecorder.stream.getTracks().forEach(t => t.stop());
}
}
clearInterval(timerInterval);
clearInterval(vadCheckInterval);
if (audioContext) { audioContext.close(); audioContext = null; }
if (analyser) { analyser = null; frequencyData = null; }
audioChunks = [];
generatedReport = null;
recordModal.style.display = 'none';
}
// Função para controlar a exibição dos estados
function showRecordState(state) {
recordStateRecording.style.display = 'none';
recordStateProcessing.style.display = 'none';
if (recordStateTranscribed) recordStateTranscribed.style.display = 'none';
recordStatePreview.style.display = 'none';
if (state === 'recording') {
recordStateRecording.style.display = 'flex';
} else if (state === 'processing') {
recordStateProcessing.style.display = 'flex';
} else if (state === 'transcribed') {
if (recordStateTranscribed) recordStateTranscribed.style.display = 'flex';
} else if (state === 'preview') {
recordStatePreview.style.display = 'flex';
}
}
// Abrir modal e iniciar gravação
if (fabRecord) {
fabRecord.addEventListener('click', async () => {
audioChunks = [];
generatedReport = null;
// Mostra estado inicial (gravando)
showRecordState('recording');
recordModal.style.display = 'flex';
// Inicia timer
recordingStartTime = Date.now();
timerInterval = setInterval(updateTimer, 1000);
updateTimer();
// Solicita permissão e inicia gravação
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
let options = {};
if (typeof MediaRecorder.isTypeSupported === 'function') {
if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
options = { mimeType: 'audio/webm;codecs=opus' };
} else if (MediaRecorder.isTypeSupported('audio/webm')) {
options = { mimeType: 'audio/webm' };
} else if (MediaRecorder.isTypeSupported('audio/ogg')) {
options = { mimeType: 'audio/ogg' };
} else if (MediaRecorder.isTypeSupported('audio/mp4')) {
options = { mimeType: 'audio/mp4' };
}
}
// Configurar AudioContext para VAD
audioContext = new AudioContext();
const source = audioContext.createMediaStreamSource(stream);
analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
source.connect(analyser);
frequencyData = new Uint8Array(analyser.frequencyBinCount);
// Iniciar verificação de silêncio (VAD)
silenceFrames = 0;
totalFrames = 0;
vadCheckInterval = setInterval(() => {
if (!analyser) return;
analyser.getByteFrequencyData(frequencyData);
const avg = frequencyData.reduce((a, b) => a + b, 0) / frequencyData.length;
totalFrames++;
if (avg < 10) {
silenceFrames++;
} else {
silenceFrames = Math.max(0, silenceFrames - 2); // recupera mais rápido
}
// Atualizar indicator visual
const recordWaveform = document.getElementById('recordWaveform');
if (recordWaveform && recordWaveform.children[0]) {
const bars = recordWaveform.children;
bars.forEach((bar) => {
const h = Math.max(4, (avg / 255) * 50 + Math.random() * 10);
bar.style.height = `${h}px`;
});
}
// Se mais de 60% dos últimos 30 frames for silêncio, avisar
if (totalFrames > 30 && (silenceFrames / 30) > 0.6) {
const btnStopRecord = document.getElementById('btnStopRecord');
if (btnStopRecord && !btnStopRecord.disabled) {
btnStopRecord.style.borderColor = '#ff6b6b';
btnStopRecord.title = 'Silêncio detectado - Clique se quiser parar';
}
} else {
const btnStopRecord = document.getElementById('btnStopRecord');
if (btnStopRecord) {
btnStopRecord.style.borderColor = '';
btnStopRecord.title = '';
}
}
}, 100);
mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) audioChunks.push(e.data);
};
mediaRecorder.start(100); // chunk a cada 100ms
} catch (err) {
console.error('Erro ao acessar microfone:', err);
alert('Não foi possível acessar o microfone. Verifique as permissões.');
closeRecordModal();
}
});
}
// Atualizar timer
function updateTimer() {
if (!recordingStartTime) return;
const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000);
const mins = Math.floor(elapsed / 60).toString().padStart(2, '0');
const secs = (elapsed % 60).toString().padStart(2, '0');
recordTimer.textContent = `${mins}:${secs}`;
}
// Parar gravação e transcrever
if (btnStopRecord) {
btnStopRecord.addEventListener('click', async () => {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
try {
mediaRecorder.stop();
} catch (e) {}
if (mediaRecorder.stream) {
mediaRecorder.stream.getTracks().forEach(t => t.stop());
}
}
clearInterval(timerInterval);
clearInterval(vadCheckInterval);
if (audioContext) { audioContext.close(); audioContext = null; }
if (analyser) { analyser = null; frequencyData = null; }
// Mostra processando com texto de transcrição
if (recordProcessingText) recordProcessingText.textContent = 'Transcrevendo áudio...';
if (recordProcessingSubtext) recordProcessingSubtext.textContent = 'Aguarde a transcrição por Whisper';
showRecordState('processing');
// Envia áudio para servidor
const blob = new Blob(audioChunks, { type: 'audio/webm' });
const formData = new FormData();
formData.append('audio', blob, 'recording.webm');
try {
const res = await fetch('/api/observacao/transcrever', {
method: 'POST',
body: formData
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.error || 'Erro na transcrição');
}
const data = await res.json();
// Exibe o texto transcrito na área de texto para edição
if (transcriptionTextarea) {
transcriptionTextarea.value = data.transcribedText || '';
}
showRecordState('transcribed');
} catch (err) {
console.error('Erro ao processar observação:', err);
alert('Erro ao transcrever áudio: ' + err.message);
closeRecordModal();
}
});
}
// Ação de analisar transcrição
if (btnAnalyzeTranscription) {
btnAnalyzeTranscription.addEventListener('click', async () => {
const text = transcriptionTextarea ? transcriptionTextarea.value.trim() : '';
if (!text || text.length < 5) {
alert('Por favor, digite ou revise a transcrição (mínimo de 5 caracteres).');
return;
}
// Mostra processando com texto de análise
if (recordProcessingText) recordProcessingText.textContent = 'Analisando observação...';
if (recordProcessingSubtext) recordProcessingSubtext.textContent = 'Gerando relatório pedagógico estruturado';
showRecordState('processing');
try {
const turmaData = localStorage.getItem('camila_turma') || '[]';
const res = await fetch('/api/observacao/analisar', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ transcribedText: text, turmaContext: JSON.parse(turmaData) })
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.error || 'Erro na análise');
}
const data = await res.json();
generatedReport = data.relatorio || data.report;
generatedCriancas = data.criancas;
generatedTurma = data.turma || 'não informada';
generatedTags = data.tags || [];
showReportPreview(data);
} catch (err) {
console.error('Erro ao analisar observação:', err);
alert('Erro ao analisar observação: ' + err.message);
showRecordState('transcribed');
}
});
}
// Ação de descartar transcrição
if (btnDiscardTranscription) {
btnDiscardTranscription.addEventListener('click', () => {
closeRecordModal();
});
}
// Mostrar prévia do relatório
function showReportPreview(data) {
showRecordState('preview');
const now = new Date();
previewDateTime.textContent = now.toLocaleString('pt-BR', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit'
});
// Monta HTML com metadados
const criancasHtml = data.criancas && data.criancas.length
? `<div class="report-meta-item"><span class="meta-label">👧 Crianças:</span> ${data.criancas.join(', ')}</div>`
: `<div class="report-meta-item"><span class="meta-label">👧 Crianças:</span> <em>não identificadas</em></div>`;
const tagsHtml = data.tags && data.tags.length
? `<div class="report-meta-item"><span class="meta-label">🏷️ Tags:</span> ${data.tags.map(t => `<span class="tag-chip">${t}</span>`).join(' ')}</div>`
: '';
const turmaHtml = data.turma && data.turma !== 'não informada'
? `<div class="report-meta-item"><span class="meta-label">🏫 Turma:</span> ${data.turma}</div>`
: '';
reportContent.innerHTML = `
<div class="report-meta">${criancasHtml}${turmaHtml}${tagsHtml}</div>
<hr style="border-color: var(--border-light); margin: 12px 0;">
${marked.parse(generatedReport || 'Relatório não disponível.')}
`;
reportContent.querySelectorAll('pre code').forEach(b => hljs.highlightElement(b));
}
// Descartar relatório
if (btnDiscardReport) {
btnDiscardReport.addEventListener('click', () => {
generatedReport = null;
closeRecordModal();
});
}
// Salvar relatório
if (btnSaveReport) {
btnSaveReport.addEventListener('click', async () => {
if (!generatedReport) return;
btnSaveReport.disabled = true;
btnSaveReport.textContent = 'Salvando...';
try {
const res = await fetch('/api/observacao/salvar', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
report: generatedReport,
criancas: generatedCriancas,
turma: generatedTurma,
tags: generatedTags
})
});
const data = await res.json();
if (data.success) {
alert('Relatório salvo com sucesso!');
} else {
alert('Erro ao salvar: ' + (data.error || 'desconhecido'));
}
} catch (err) {
console.error('Erro ao salvar:', err);
alert('Erro ao salvar o relatório.');
} finally {
btnSaveReport.disabled = false;
btnSaveReport.textContent = 'Salvar';
closeRecordModal();
}
});
}
// Sair / descartar
if (btnExitRecord) {
btnExitRecord.addEventListener('click', closeRecordModal);
}
// Fechar clicando fora
if (recordModal) {
recordModal.addEventListener('click', (e) => {
if (e.target === recordModal) closeRecordModal();
});
}
// ============================================================
// MODAL: MINHAS OBSERVAÇÕES
// ============================================================
const obsModal = document.getElementById('obsModal');
const btnObservacoes = document.getElementById('btnObservacoes');
const btnObsEspeciais = document.getElementById('btnObsEspeciais');
const btnCloseObsModal = document.getElementById('btnCloseObsModal');
const obsModalTitle = document.getElementById('obsModalTitle');
const obsList = document.getElementById('obsList');
const obsFilterMes = document.getElementById('obsFilterMes');
const obsFilterTag = document.getElementById('obsFilterTag');
const obsSearchCrianca = document.getElementById('obsSearchCrianca');
let allObservations = [];
let isSpecialObsMode = false;
// Abrir modal de observações
if (btnObservacoes) {
btnObservacoes.addEventListener('click', async () => {
isSpecialObsMode = false;
if (obsModalTitle) obsModalTitle.textContent = '📒 Minhas Observações';
obsModal.style.display = 'flex';
obsList.innerHTML = '<div class="obs-empty">Carregando...</div>';
try {
const res = await fetch('/api/observacoes');
const data = await res.json();
allObservations = data.observations || [];
populateMonthFilter();
renderObsList();
} catch (err) {
console.error('Erro ao carregar observações:', err);
obsList.innerHTML = '<div class="obs-empty">Erro ao carregar observações.</div>';
}
});
}
// Abrir modal de observações Especiais
if (btnObsEspeciais) {
btnObsEspeciais.addEventListener('click', async () => {
isSpecialObsMode = true;
if (obsModalTitle) obsModalTitle.textContent = '📒 Observações Educação Especial';
obsModal.style.display = 'flex';
obsList.innerHTML = '<div class="obs-empty">Carregando...</div>';
try {
const res = await fetch('/api/observacoes');
const data = await res.json();
allObservations = data.observations || [];
populateMonthFilter();
renderObsList();
} catch (err) {
console.error('Erro ao carregar observações:', err);
obsList.innerHTML = '<div class="obs-empty">Erro ao carregar observações.</div>';
}
});
}
// Fechar modal
if (btnCloseObsModal) {
btnCloseObsModal.addEventListener('click', () => {
obsModal.style.display = 'none';
});
}
if (obsModal) {
obsModal.addEventListener('click', (e) => {
if (e.target === obsModal) obsModal.style.display = 'none';
});
}
// Filtros
if (obsFilterMes) obsFilterMes.addEventListener('change', renderObsList);
if (obsFilterTag) obsFilterTag.addEventListener('change', renderObsList);
if (obsSearchCrianca) obsSearchCrianca.addEventListener('input', renderObsList);
// Preenche select de meses
function populateMonthFilter() {
if (!obsFilterMes) return;
const months = [...new Set(allObservations.map(o => o.date?.slice(0, 7)))]
.filter(Boolean).sort().reverse();
obsFilterMes.innerHTML = '<option value="">Todos os meses</option>' +
months.map(m => `<option value="${m}">${m.split('-')[1]}/${m.split('-')[0]}</option>`).join('');
}
// Renderiza lista filtrada
function renderObsList() {
if (!obsList) return;
const mes = obsFilterMes?.value || '';
const tag = obsFilterTag?.value || '';
const search = obsSearchCrianca?.value.toLowerCase() || '';
let especialKidsNames = [];
if (isSpecialObsMode) {
const turma = JSON.parse(localStorage.getItem('camila_turma') || '[]');
especialKidsNames = turma.filter(c => c.especial).map(c => (c.nome || '').toLowerCase().trim()).filter(Boolean);
if (especialKidsNames.length === 0) {
obsList.innerHTML = '<div class="obs-empty">Nenhuma criança com "Educação Especial" cadastrada na Minha Turma.</div>';
return;
}
}
const filtered = allObservations.filter(o => {
if (mes && !o.date?.startsWith(mes)) return false;
if (tag && !o.tags?.includes(tag)) return false;
const nomes = (o.criancas || []).join(' ').toLowerCase();
if (isSpecialObsMode) {
// Must contain at least one special kid's name
const preview = (o.preview || '').toLowerCase();
const isEspecial = especialKidsNames.some(ekn => nomes.includes(ekn) || preview.includes(ekn));
if (!isEspecial) return false;
}
if (search) {
if (!nomes.includes(search) && !(o.preview || '').toLowerCase().includes(search)) return false;
}
return true;
});
if (filtered.length === 0) {
obsList.innerHTML = '<div class="obs-empty">Nenhuma observação encontrada.</div>';
return;
}
obsList.innerHTML = filtered.map(o => {
const tagsHtml = (o.tags || []).map(t => `<span class="obs-tag">${t}</span>`).join('');
const criancasHtml = (o.criancas || []).map(c => `<span class="obs-crianca">${c}</span>`).join('');
const dateFormatted = o.date ? o.date.split('-').reverse().join('/') : '';
return `
<div class="obs-item" data-filename="${o.filename}" data-yearmonth="${o.date?.slice(0, 7)}">
<div class="obs-item-header">
<strong>📋 ${dateFormatted}${o.time ? ' às ' + o.time : ''}</strong>
${o.turma && o.turma !== 'não informada' ? `<span class="obs-crianca">🏫 ${o.turma}</span>` : ''}
</div>
<div class="obs-item-preview">${o.preview || 'Sem prévia disponível.'}</div>
<div class="obs-item-footer">${tagsHtml}${criancasHtml}</div>
</div>
`;
}).join('');
// Click para abrir observação
obsList.querySelectorAll('.obs-item').forEach(item => {
item.addEventListener('click', async () => {
const ym = item.dataset.yearmonth;
const fn = item.dataset.filename;
if (!ym || !fn) return;
try {
const res = await fetch(`/api/observacoes/${ym}/${fn}`);
const text = await res.text();
showObsDetail(text);
} catch (err) {
console.error('Erro ao carregar observação:', err);
}
});
});
};
// ============================================================
// MODAL: MINHA TURMA (COM MÚLTIPLAS TURMAS E SUPABASE)
// ============================================================
const minhaTurmaModal = document.getElementById('minhaTurmaModal');
const btnMinhaTurma = document.getElementById('btnMinhaTurma');
const btnCloseMinhaTurmaModal = document.getElementById('btnCloseMinhaTurmaModal');
// Turmas List & Form
const turmasList = document.getElementById('turmasList');
const btnCreateTurma = document.getElementById('btnCreateTurma');
const btnCancelTurma = document.getElementById('btnCancelTurma');
const btnSaveTurmaConfig = document.getElementById('btnSaveTurmaConfig');
const configTurmaId = document.getElementById('configTurmaId');
const configTurmaNome = document.getElementById('configTurmaNome');
const configTurmaSala = document.getElementById('configTurmaSala');
const configTurmaPeriodo = document.getElementById('configTurmaPeriodo');
const configTurmaProfTitular = document.getElementById('configTurmaProfTitular');
const configTurmaProfAux1 = document.getElementById('configTurmaProfAux1');
const configTurmaProfAux2 = document.getElementById('configTurmaProfAux2');
const configTurmaCuidadora = document.getElementById('configTurmaCuidadora');
const turmaFormTitle = document.getElementById('turmaFormTitle');
// Alunos List & Form
const filterAlunosTurma = document.getElementById('filterAlunosTurma');
const childrenList = document.getElementById('childrenList');
const btnCreateChild = document.getElementById('btnCreateChild');
const btnCancelChild = document.getElementById('btnCancelChild');
const btnSaveChild = document.getElementById('btnSaveChild');
const childFormTitle = document.getElementById('childFormTitle');
const cFormId = document.getElementById('childFormId');
const cFormTurma = document.getElementById('childFormTurma');
const cFormNome = document.getElementById('childFormNome');
const cFormApelido = document.getElementById('childFormApelido');
const cFormDataNasc = document.getElementById('childFormDataNasc');
const cFormEspecial = document.getElementById('childFormEspecial');
const divEspecialDetalhes = document.getElementById('divEspecialDetalhes');
const cFormEspecialDetalhes = document.getElementById('childFormEspecialDetalhes');
const cFormPais = document.getElementById('childFormPais');
const cFormAutorizados = document.getElementById('childFormAutorizados');
// Tabs
const tabTurmaConfig = document.getElementById('tabTurmaConfig');
const tabTurmaAlunos = document.getElementById('tabTurmaAlunos');
const turmaConfigSection = document.getElementById('turmaConfigSection');
const turmaAlunosSection = document.getElementById('turmaAlunosSection');
let currentTurmas = [];
let currentAlunos = [];
// API Calls
const fetchTurmas = async () => {
try {
const res = await fetch('/api/turmas');
currentTurmas = await res.json();
populateTurmaSelects();
return currentTurmas;
} catch(e) { console.error(e); return []; }
};
const fetchAlunos = async () => {
try {
const res = await fetch('/api/alunos');
currentAlunos = await res.json();
localStorage.setItem('camila_turma', JSON.stringify(currentAlunos));
return currentAlunos;
} catch(e) { console.error(e); return []; }
};
const populateTurmaSelects = () => {
const opts = '<option value="">Todas as Turmas</option>' + currentTurmas.map(t => `<option value="${t.id}">${t.nome}</option>`).join('');
if(filterAlunosTurma) filterAlunosTurma.innerHTML = opts;
const formOpts = '<option value="">Selecione uma turma...</option>' + currentTurmas.map(t => `<option value="${t.id}">${t.nome}</option>`).join('');
if(cFormTurma) cFormTurma.innerHTML = formOpts;
if(document.getElementById('emitirFilterTurma')) {
document.getElementById('emitirFilterTurma').innerHTML = '<option value="">Todas as turmas</option>' + currentTurmas.map(t => `<option value="${t.id}">${t.nome}</option>`).join('');
}
};
// Tabs Logic
if (tabTurmaConfig) {
tabTurmaConfig.addEventListener('click', () => {
tabTurmaConfig.classList.add('active');
tabTurmaConfig.style.borderBottomColor = 'var(--brand-green)';
tabTurmaConfig.style.color = 'var(--brand-green)';
tabTurmaAlunos.classList.remove('active');
tabTurmaAlunos.style.borderBottomColor = 'transparent';
tabTurmaAlunos.style.color = 'var(--text-secondary)';
turmaConfigSection.style.display = 'flex';
turmaAlunosSection.style.display = 'none';
});
}
if (tabTurmaAlunos) {
tabTurmaAlunos.addEventListener('click', () => {
tabTurmaAlunos.classList.add('active');
tabTurmaAlunos.style.borderBottomColor = 'var(--brand-green)';
tabTurmaAlunos.style.color = 'var(--brand-green)';
tabTurmaConfig.classList.remove('active');
tabTurmaConfig.style.borderBottomColor = 'transparent';
tabTurmaConfig.style.color = 'var(--text-secondary)';
turmaAlunosSection.style.display = 'flex';
turmaConfigSection.style.display = 'none';
});
}
// Turmas Logic
const clearTurmaForm = () => {
configTurmaId.value = '';
configTurmaNome.value = '';
configTurmaSala.value = '';
configTurmaPeriodo.value = 'Integral';
configTurmaProfTitular.value = '';
configTurmaProfAux1.value = '';
configTurmaProfAux2.value = '';
configTurmaCuidadora.value = '';
turmaFormTitle.textContent = 'Cadastrar Turma';
};
const renderTurmasList = () => {
if(!turmasList) return;
if(currentTurmas.length === 0) {
turmasList.innerHTML = '<div style="color: var(--text-secondary); text-align: center; padding: 20px;">Nenhuma turma cadastrada.</div>';
return;
}
turmasList.innerHTML = currentTurmas.map(t => `
<div style="background: var(--bg-secondary); border: 1px solid var(--border-light); padding: 12px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-weight: 600; color: var(--text-primary);">${escapeHtml(t.nome)}</div>
<div style="font-size: 0.8rem; color: var(--text-secondary);">${t.periodo || ''} ${t.sala ? '- ' + t.sala : ''}</div>
</div>
<div style="display: flex; gap: 8px;">
<button onclick="editTurma('${t.id}')" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 4px;" title="Editar">✏️</button>
<button onclick="deleteTurma('${t.id}')" style="background: none; border: none; color: #ef4444; cursor: pointer; padding: 4px;" title="Excluir">🗑️</button>
</div>
</div>
`).join('');
};
window.editTurma = (id) => {
const t = currentTurmas.find(x => x.id === id);
if(!t) return;
configTurmaId.value = t.id;
configTurmaNome.value = t.nome || '';
configTurmaSala.value = t.sala || '';
configTurmaPeriodo.value = t.periodo || 'Integral';
configTurmaProfTitular.value = t.prof_titular || '';
configTurmaProfAux1.value = t.prof_aux1 || '';
configTurmaProfAux2.value = t.prof_aux2 || '';
configTurmaCuidadora.value = t.cuidadora || '';
turmaFormTitle.textContent = 'Editar Turma';
};
window.deleteTurma = async (id) => {
const isOk = await showCustomConfirm('Excluir Turma', 'Deseja excluir esta turma? Todos os alunos associados a ela também serão excluídos.', true);
if(isOk) {
await fetch('/api/turmas/' + id, { method: 'DELETE' });
await fetchTurmas();
await fetchAlunos();
renderTurmasList();
renderAlunosList();
clearTurmaForm();
}
};
if(btnCreateTurma) btnCreateTurma.addEventListener('click', clearTurmaForm);
if(btnCancelTurma) btnCancelTurma.addEventListener('click', clearTurmaForm);
if (btnSaveTurmaConfig) {
btnSaveTurmaConfig.addEventListener('click', async () => {
const nome = configTurmaNome.value.trim();
if(!nome) { showCustomAlert('Aviso', 'O Nome da Turma é obrigatório.'); return; }
const body = {
nome,
sala: configTurmaSala.value,
periodo: configTurmaPeriodo.value,
profTitular: configTurmaProfTitular.value,
profAux1: configTurmaProfAux1.value,
profAux2: configTurmaProfAux2.value,
cuidadora: configTurmaCuidadora.value
};
const id = configTurmaId.value;
const url = id ? '/api/turmas/' + id : '/api/turmas';
const method = id ? 'PUT' : 'POST';
try {
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if(res.ok) {
showCustomAlert('Sucesso', 'Turma salva com sucesso!');
await fetchTurmas();
renderTurmasList();
clearTurmaForm();
} else {
showCustomAlert('Erro', 'Falha ao salvar turma.');
}
} catch(e) { console.error(e); }
});
}
// Alunos Logic
const clearChildForm = () => {
cFormId.value = '';
cFormTurma.value = filterAlunosTurma ? filterAlunosTurma.value : '';
cFormNome.value = '';
cFormApelido.value = '';
cFormDataNasc.value = '';
cFormEspecial.checked = false;
cFormEspecialDetalhes.value = '';
divEspecialDetalhes.style.display = 'none';
cFormPais.value = '';
cFormAutorizados.value = '';
childFormTitle.textContent = 'Cadastrar Criança';
};
if (cFormEspecial) {
cFormEspecial.addEventListener('change', () => {
divEspecialDetalhes.style.display = cFormEspecial.checked ? 'flex' : 'none';
});
}
const renderAlunosList = () => {
if (!childrenList) return;
const filterTurmaId = filterAlunosTurma ? filterAlunosTurma.value : '';
let filtered = currentAlunos;
if(filterTurmaId) {
filtered = currentAlunos.filter(a => a.turma_id === filterTurmaId);
}
if (filtered.length === 0) {
childrenList.innerHTML = '<div style="color: var(--text-secondary); text-align: center; padding: 20px;">Nenhuma criança encontrada.</div>';
return;
}
filtered.sort((a, b) => (a.nome || '').localeCompare(b.nome || ''));
childrenList.innerHTML = filtered.map(c => {
const turmaObj = currentTurmas.find(t => t.id === c.turma_id);
const turmaNome = turmaObj ? turmaObj.nome : 'Sem turma';
return `
<div style="background: var(--bg-secondary); border: 1px solid var(--border-light); padding: 12px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-weight: 600; color: var(--text-primary);">${escapeHtml(c.nome)} ${c.especial ? '⭐' : ''}</div>
<div style="font-size: 0.8rem; color: var(--text-secondary);">${escapeHtml(turmaNome)} ${c.apelido ? '- ' + escapeHtml(c.apelido) : ''}</div>
</div>
<div style="display: flex; gap: 8px;">
<button onclick="editChild('${c.id}')" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 4px;" title="Editar">✏️</button>
<button onclick="deleteChild('${c.id}')" style="background: none; border: none; color: #ef4444; cursor: pointer; padding: 4px;" title="Excluir">🗑️</button>
</div>
</div>
`}).join('');
};
if(filterAlunosTurma) {
filterAlunosTurma.addEventListener('change', () => {
renderAlunosList();
if(cFormTurma) cFormTurma.value = filterAlunosTurma.value;
});
}
window.editChild = (id) => {
const c = currentAlunos.find(x => x.id === id);
if (!c) return;
cFormId.value = c.id;
cFormTurma.value = c.turma_id || '';
cFormNome.value = c.nome || '';
cFormApelido.value = c.apelido || '';
if(c.data_nasc) {
cFormDataNasc.value = c.data_nasc.split('T')[0];
} else {
cFormDataNasc.value = '';
}
cFormEspecial.checked = !!c.especial;
cFormEspecialDetalhes.value = c.especial_detalhes || '';
divEspecialDetalhes.style.display = c.especial ? 'flex' : 'none';
cFormPais.value = c.pais || '';
cFormAutorizados.value = c.autorizados || '';
childFormTitle.textContent = 'Editar Criança';
};
window.deleteChild = async (id) => {
const isOk = await showCustomConfirm('Excluir', 'Deseja excluir esta criança? Isso não removerá as observações já feitas.', true);
if (isOk) {
await fetch('/api/alunos/' + id, { method: 'DELETE' });
await fetchAlunos();
renderAlunosList();
clearChildForm();
}
};
if (btnMinhaTurma) {
btnMinhaTurma.addEventListener('click', async () => {
minhaTurmaModal.style.display = 'flex';
await fetchTurmas();
await fetchAlunos();
renderTurmasList();
renderAlunosList();
clearTurmaForm();
clearChildForm();
});
}
if (btnCloseMinhaTurmaModal) btnCloseMinhaTurmaModal.addEventListener('click', () => minhaTurmaModal.style.display = 'none');
if (minhaTurmaModal) {
minhaTurmaModal.addEventListener('click', (e) => {
if (e.target === minhaTurmaModal) minhaTurmaModal.style.display = 'none';
});
}
if (btnCreateChild) btnCreateChild.addEventListener('click', clearChildForm);
if (btnCancelChild) btnCancelChild.addEventListener('click', clearChildForm);
if (btnSaveChild) {
btnSaveChild.addEventListener('click', async () => {
if (!cFormNome.value.trim() || !cFormTurma.value) {
showCustomAlert('Aviso', 'O Nome da Criança e a Turma são obrigatórios.');
return;
}
const id = cFormId.value;
const body = {
turma_id: cFormTurma.value,
nome: cFormNome.value.trim(),
apelido: cFormApelido.value.trim(),
data_nasc: cFormDataNasc.value || null,
especial: cFormEspecial.checked,
especial_detalhes: cFormEspecialDetalhes.value.trim(),
pais: cFormPais.value.trim(),
autorizados: cFormAutorizados.value.trim()
};
const url = id ? '/api/alunos/' + id : '/api/alunos';
const method = id ? 'PUT' : 'POST';
try {
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if(res.ok) {
await fetchAlunos();
renderAlunosList();
clearChildForm();
showCustomAlert('Sucesso', 'Criança salva com sucesso!');
} else {
showCustomAlert('Erro', 'Falha ao salvar criança.');
}
} catch(e) { console.error(e); }
});
}
// Preencher com Voz
const btnVoiceRecordChild = document.getElementById('btnVoiceRecordChild');
if (btnVoiceRecordChild && (window.SpeechRecognition || window.webkitSpeechRecognition)) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const formRecognition = new SpeechRecognition();
formRecognition.lang = 'pt-BR';
formRecognition.continuous = false;
formRecognition.interimResults = false;
formRecognition.onstart = () => {
btnVoiceRecordChild.innerHTML = '🛑 Gravando... (Clique para parar)';
btnVoiceRecordChild.style.background = 'rgba(239, 68, 68, 0.1)';
btnVoiceRecordChild.style.color = '#ef4444';
btnVoiceRecordChild.style.borderColor = '#ef4444';
};
formRecognition.onresult = async (event) => {
const transcript = event.results[0][0].transcript;
btnVoiceRecordChild.innerHTML = '⏳ Processando com IA...';
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [{
role: 'user',
content: `A professora ditou as seguintes informações para cadastrar uma criança na turma: "${transcript}".
Por favor, extraia os dados e retorne APENAS um JSON válido com os seguintes campos:
"nome", "apelido", "data_nasc" (formato YYYY-MM-DD), "especial" (boolean), "especial_detalhes", "pais", "autorizados".
Se algum dado não for mencionado, deixe a string vazia ou false para boolean.`
}],
temperature: 0.1
})
});
if (response.ok) {
const data = await response.json();
const assistantReply = data.message;
const match = assistantReply.match(/```json\n([\s\S]*?)\n```/) || assistantReply.match(/\{[\s\S]*\}/);
if (match) {
const parsed = JSON.parse(match[1] || match[0]);
if (parsed.nome) cFormNome.value = parsed.nome;
if (parsed.apelido) cFormApelido.value = parsed.apelido;
if (parsed.data_nasc) cFormDataNasc.value = parsed.data_nasc;
if (parsed.especial) cFormEspecial.checked = parsed.especial;
divEspecialDetalhes.style.display = cFormEspecial.checked ? 'flex' : 'none';
if (parsed.especial_detalhes) cFormEspecialDetalhes.value = parsed.especial_detalhes;
if (parsed.pais) cFormPais.value = parsed.pais;
if (parsed.autorizados) cFormAutorizados.value = parsed.autorizados;
}
}
} catch (err) {
console.error('Erro ao processar voz:', err);
showCustomAlert('Erro', 'Não foi possível extrair os dados. Tente novamente ou preencha manualmente.');
}
};
formRecognition.onend = () => {
btnVoiceRecordChild.innerHTML = '🎤 Preencher com Voz';
btnVoiceRecordChild.style.background = 'rgba(16, 163, 127, 0.1)';
btnVoiceRecordChild.style.color = 'var(--brand-green)';
btnVoiceRecordChild.style.borderColor = 'var(--brand-green)';
};
btnVoiceRecordChild.addEventListener('click', () => {
if (btnVoiceRecordChild.innerHTML.includes('Gravando')) {
formRecognition.stop();
} else {
formRecognition.start();
}
});
}
// Mostra detalhe da observação em modal
const showObsDetail = (mdText) => {
const modal = document.createElement('div');
modal.className = 'settings-modal';
modal.style.display = 'flex';
modal.innerHTML = `
<div class="settings-modal-content obs-modal-content" style="max-width: 680px;">
<div class="settings-modal-header">
<h3>📋 Observação</h3>
<button class="btn-close-modal" onclick="this.closest('.settings-modal').remove()">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div>
<div style="padding: 20px; overflow-y: auto; max-height: 70vh;">
<div class="obs-detail-content" style="color: var(--text-primary); line-height: 1.6;">
${marked.parse(mdText.replace(/^---[\s\S]*?---\n/, ''))}
</div>
<div style="margin-top: 16px; display: flex; gap: 8px;">
<button class="btn-save-report" onclick="navigator.clipboard.writeText(this.closest('.settings-modal').querySelector('.obs-detail-content').innerText).then(()=>alert('Copiado!'))">Copiar Texto</button>
<button class="btn-discard" onclick="this.closest('.settings-modal').remove()">Fechar</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.remove();
});
};
// // ============================================================
// CRUD DE MODELOS DE RELATÓRIO
// ============================================================
const modelosRelatorioModal = document.getElementById('modelosRelatorioModal');
const btnModelosRelatorio = document.getElementById('btnModelosRelatorio');
const btnCloseModelosModal = document.getElementById('btnCloseModelosModal');
const templatesList = document.getElementById('templatesList');
const btnCreateTemplate = document.getElementById('btnCreateTemplate');
const btnCancelTemplate = document.getElementById('btnCancelTemplate');
const btnSaveTemplate = document.getElementById('btnSaveTemplate');
const templateFormId = document.getElementById('templateFormId');
const templateFormNome = document.getElementById('templateFormNome');
const templateFormFinalidade = document.getElementById('templateFormFinalidade');
const templateFormPeriodicidade = document.getElementById('templateFormPeriodicidade');
const templateFormEstrutura = document.getElementById('templateFormEstrutura');
const templateFormTitle = document.getElementById('templateFormTitle');
let allTemplates = [];
// Abrir modal de modelos
if (btnModelosRelatorio) {
btnModelosRelatorio.addEventListener('click', async () => {
modelosRelatorioModal.style.display = 'flex';
clearTemplateForm();
await fetchTemplates();
});
}
// Fechar modal de modelos
if (btnCloseModelosModal) {
btnCloseModelosModal.addEventListener('click', () => {
modelosRelatorioModal.style.display = 'none';
});
}
if (modelosRelatorioModal) {
modelosRelatorioModal.addEventListener('click', (e) => {
if (e.target === modelosRelatorioModal) modelosRelatorioModal.style.display = 'none';
});
}
// Buscar modelos do backend
async function fetchTemplates() {
templatesList.innerHTML = '<div style="color: var(--text-secondary); text-align: center; padding: 20px;">Carregando...</div>';
try {
const res = await fetch('/api/modelos');
allTemplates = await res.json();
renderTemplatesList();
} catch (err) {
console.error('Erro ao buscar modelos:', err);
templatesList.innerHTML = '<div style="color: #ff6b6b; text-align: center; padding: 20px;">Erro ao carregar modelos.</div>';
}
}
// Renderizar a lista de modelos na esquerda
function renderTemplatesList() {
if (allTemplates.length === 0) {
templatesList.innerHTML = '<div style="color: var(--text-secondary); text-align: center; padding: 20px;">Nenhum modelo cadastrado.</div>';
return;
}
templatesList.innerHTML = allTemplates.map(t => `
<div class="obs-item" style="padding: 10px; margin: 0 0 8px 0; cursor: pointer; border-left: 3px solid var(--brand-green); display: flex; flex-direction: column; gap: 4px; background: var(--bg-secondary); border-radius: 8px;" data-id="${t.id}">
<div style="font-weight: 600; color: var(--text-primary); font-size: 0.9rem;">${t.nome}</div>
<div style="font-size: 0.75rem; color: var(--text-secondary);">${t.periodicidade}${t.finalidade || 'Sem finalidade'}</div>
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px;">
<button class="template-duplicate-btn" data-id="${t.id}" style="background: none; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 2px 8px; border-radius: 4px; font-size: 0.72rem; cursor: pointer; transition: opacity 0.2s;">Duplicar</button>
<button class="template-edit-btn" data-id="${t.id}" style="background: none; border: 1px solid var(--border-light); color: var(--brand-green); padding: 2px 8px; border-radius: 4px; font-size: 0.72rem; cursor: pointer;">Editar</button>
<button class="template-delete-btn" data-id="${t.id}" style="background: none; border: 1px solid var(--border-light); color: #ff6b6b; padding: 2px 8px; border-radius: 4px; font-size: 0.72rem; cursor: pointer;">Excluir</button>
</div>
</div>
`).join('');
// Eventos de clique na lista para editar/deletar/duplicar
templatesList.querySelectorAll('.obs-item').forEach(item => {
item.addEventListener('click', (e) => {
if (e.target.tagName === 'BUTTON') return;
const id = item.dataset.id;
const template = allTemplates.find(t => t.id == id);
if (template) fillTemplateForm(template);
});
});
templatesList.querySelectorAll('.template-duplicate-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const id = btn.dataset.id;
const template = allTemplates.find(t => t.id == id);
if (template) {
fillTemplateForm(template);
templateFormId.value = '';
templateFormNome.value = template.nome + ' (Cópia)';
templateFormTitle.textContent = 'Duplicar Modelo';
if (templateFormNome) templateFormNome.focus();
}
});
});
templatesList.querySelectorAll('.template-edit-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const id = btn.dataset.id;
const template = allTemplates.find(t => t.id == id);
if (template) fillTemplateForm(template);
});
});
templatesList.querySelectorAll('.template-delete-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const id = btn.dataset.id;
const template = allTemplates.find(t => t.id == id);
if (!template) return;
if (confirm(`Tem certeza que deseja excluir o modelo "${template.nome}"?`)) {
try {
const res = await fetch(`/api/modelos/${id}`, { method: 'DELETE' });
if (res.ok) {
alert('Modelo excluído com sucesso!');
clearTemplateForm();
await fetchTemplates();
} else {
const err = await res.json();
alert('Erro ao excluir: ' + err.error);
}
} catch (err) {
console.error('Erro ao excluir modelo:', err);
}
}
});
});
}
// Preencher formulário de edição
function fillTemplateForm(template) {
templateFormId.value = template.id;
templateFormNome.value = template.nome;
templateFormFinalidade.value = template.finalidade || '';
templateFormPeriodicidade.value = template.periodicidade || 'Semanal';
templateFormEstrutura.value = template.estrutura || '';
templateFormTitle.textContent = 'Editar Modelo';
}
// Limpar formulário
function clearTemplateForm() {
templateFormId.value = '';
templateFormNome.value = '';
templateFormFinalidade.value = '';
templateFormPeriodicidade.value = 'Semanal';
templateFormEstrutura.value = '';
templateFormTitle.textContent = 'Novo Modelo';
}
// Botão "Novo Modelo"
if (btnCreateTemplate) {
btnCreateTemplate.addEventListener('click', clearTemplateForm);
}
// Botão "Cancelar"
if (btnCancelTemplate) {
btnCancelTemplate.addEventListener('click', () => {
clearTemplateForm();
if (modelosRelatorioModal) modelosRelatorioModal.style.display = 'none';
});
}
// Botão "Salvar Modelo"
if (btnSaveTemplate) {
btnSaveTemplate.addEventListener('click', async () => {
const id = templateFormId.value;
const nome = templateFormNome.value.trim();
const finalidade = templateFormFinalidade.value.trim();
const periodicidade = templateFormPeriodicidade.value;
const estrutura = templateFormEstrutura.value.trim();
if (!nome || !estrutura) {
alert('Nome e estrutura do modelo são obrigatórios.');
return;
}
const body = { nome, finalidade, periodicidade, estrutura };
const url = id ? `/api/modelos/${id}` : '/api/modelos';
const method = id ? 'PUT' : 'POST';
try {
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) {
alert('Modelo salvo com sucesso!');
clearTemplateForm();
await fetchTemplates();
} else {
const err = await res.json();
alert('Erro ao salvar modelo: ' + err.error);
}
} catch (err) {
console.error('Erro ao salvar modelo:', err);
}
});
}
// ============================================================
// GERAÇÃO / EMISSÃO DE RELATÓRIO PEDAGÓGICO
// ============================================================
const emitirRelatorioModal = document.getElementById('emitirRelatorioModal');
const btnEmitirRelatorio = document.getElementById('btnEmitirRelatorio');
const btnCloseEmitirModal = document.getElementById('btnCloseEmitirModal');
const emitirFilterAno = document.getElementById('emitirFilterAno');
const emitirFilterTurma = document.getElementById('emitirFilterTurma');
const emitirFilterCrianca = document.getElementById('emitirFilterCrianca');
const emitirFilterDataInicio = document.getElementById('emitirFilterDataInicio');
const emitirFilterDataFim = document.getElementById('emitirFilterDataFim');
const emitirTagsContainer = document.getElementById('emitirTagsContainer');
const emitirFilterTemplate = document.getElementById('emitirFilterTemplate');
let emitirSelectedTags = [];
const emitirObsCountInfo = document.getElementById('emitirObsCountInfo');
const btnGenerateCompiledReport = document.getElementById('btnGenerateCompiledReport');
const emitirReportPreviewArea = document.getElementById('emitirReportPreviewArea');
const btnExportDocx = document.getElementById('btnExportDocx');
const btnExportPdf = document.getElementById('btnExportPdf');
let activeCompiledReport = null; // Guarda o texto do relatório compilado ativo
// Abrir modal de emissão
if (btnEmitirRelatorio) {
btnEmitirRelatorio.addEventListener('click', async () => {
emitirRelatorioModal.style.display = 'flex';
activeCompiledReport = null;
emitirReportPreviewArea.innerHTML = `
<div style="color: var(--text-secondary); text-align: center; margin-top: 50px;">
Escolha uma criança e um modelo de relatório para começar.
</div>
`;
btnExportDocx.disabled = true;
btnExportDocx.style.cursor = 'not-allowed';
btnExportDocx.style.borderColor = 'var(--border-light)';
btnExportDocx.style.color = 'var(--text-secondary)';
btnExportPdf.disabled = true;
btnExportPdf.style.cursor = 'not-allowed';
btnExportPdf.style.borderColor = 'var(--border-light)';
btnExportPdf.style.color = 'var(--text-secondary)';
emitirFilterDataInicio.value = '';
emitirFilterDataFim.value = '';
emitirSelectedTags = [];
if (emitirTagsContainer) {
emitirTagsContainer.querySelectorAll('.tag-pill').forEach(p => p.classList.remove('active'));
}
await loadFiltersData();
});
}
// Fechar modal de emissão
if (btnCloseEmitirModal) {
btnCloseEmitirModal.addEventListener('click', () => {
emitirRelatorioModal.style.display = 'none';
});
}
if (emitirRelatorioModal) {
emitirRelatorioModal.addEventListener('click', (e) => {
if (e.target === emitirRelatorioModal) emitirRelatorioModal.style.display = 'none';
});
}
// Buscar crianças com base nos filtros de Ano e Turma selecionados
async function loadChildrenList() {
try {
const activeAno = emitirFilterAno ? emitirFilterAno.value : '';
const activeTurma = emitirFilterTurma ? emitirFilterTurma.value : '';
const activeCrianca = emitirFilterCrianca ? emitirFilterCrianca.value : '';
let url = '/api/observacoes/criancas';
const params = [];
if (activeAno) params.push(`ano=${activeAno}`);
if (activeTurma) params.push(`turma=${encodeURIComponent(activeTurma)}`);
if (params.length > 0) url += '?' + params.join('&');
const res = await fetch(url);
const criancas = await res.json();
emitirFilterCrianca.innerHTML = '<option value="">Selecione a criança</option>' +
`<option value="todas" ${activeCrianca === 'todas' ? 'selected' : ''}>Todas as crianças (Relatório Global)</option>` +
(Array.isArray(criancas) ? criancas : []).map(c => `<option value="${c}" ${c === activeCrianca ? 'selected' : ''}>${c}</option>`).join('');
} catch (err) {
console.error('Erro ao carregar lista de crianças:', err);
}
}
// Carregar dados de filtros (anos, turmas, crianças e templates)
async function loadFiltersData() {
try {
const activeAno = emitirFilterAno ? emitirFilterAno.value : '';
const activeTurma = emitirFilterTurma ? emitirFilterTurma.value : '';
const res = await fetch('/api/observacoes/metadata');
const meta = await res.json();
if (emitirFilterAno) {
emitirFilterAno.innerHTML = '<option value="">Todos os anos</option>' +
(meta.anos || []).map(y => `<option value="${y}" ${y === activeAno ? 'selected' : ''}>${y}</option>`).join('');
}
if (emitirFilterTurma) {
emitirFilterTurma.innerHTML = '<option value="">Todas as turmas</option>' +
(meta.turmas || []).map(t => `<option value="${t}" ${t === activeTurma ? 'selected' : ''}>${t}</option>`).join('');
}
} catch (err) {
console.error('Erro ao buscar metadados de filtros:', err);
}
await loadChildrenList();
try {
const activeTemplate = emitirFilterTemplate ? emitirFilterTemplate.value : '';
const res = await fetch('/api/modelos');
const modelos = await res.json();
emitirFilterTemplate.innerHTML = '<option value="">Selecione o modelo</option>' +
(Array.isArray(modelos) ? modelos : []).map(m => `<option value="${m.id}" ${m.id == activeTemplate ? 'selected' : ''}>${m.nome}</option>`).join('');
} catch (err) {
console.error('Erro ao buscar modelos para filtros:', err);
}
updateObservationsCount();
}
// Ouvintes para atualizar contagem e filtros de criança
if (emitirFilterAno) {
emitirFilterAno.addEventListener('change', async () => {
await loadChildrenList();
updateObservationsCount();
});
}
if (emitirFilterTurma) {
emitirFilterTurma.addEventListener('change', async () => {
await loadChildrenList();
updateObservationsCount();
});
}
[emitirFilterCrianca, emitirFilterDataInicio, emitirFilterDataFim, emitirFilterTemplate].forEach(el => {
if (el) el.addEventListener('change', updateObservationsCount);
});
if (emitirFilterCrianca) {
emitirFilterCrianca.addEventListener('input', updateObservationsCount);
}
// Adicionar ouvintes para as pills de tags
if (emitirTagsContainer) {
emitirTagsContainer.querySelectorAll('.tag-pill').forEach(pill => {
pill.addEventListener('click', () => {
const tag = pill.getAttribute('data-tag');
if (pill.classList.contains('active')) {
pill.classList.remove('active');
emitirSelectedTags = emitirSelectedTags.filter(t => t !== tag);
} else {
pill.classList.add('active');
emitirSelectedTags.push(tag);
}
updateObservationsCount();
});
});
}
// Recalcular quantidade de observações elegíveis
async function updateObservationsCount() {
const crianca = emitirFilterCrianca ? emitirFilterCrianca.value : '';
const dataInicio = emitirFilterDataInicio ? emitirFilterDataInicio.value : '';
const dataFim = emitirFilterDataFim ? emitirFilterDataFim.value : '';
const tag = emitirSelectedTags.join(',');
const modeloId = emitirFilterTemplate ? emitirFilterTemplate.value : '';
const ano = emitirFilterAno ? emitirFilterAno.value : '';
const turma = emitirFilterTurma ? emitirFilterTurma.value : '';
if (!crianca || !modeloId) {
emitirObsCountInfo.textContent = 'Escolha uma criança (ou "Todas") e um modelo para prosseguir.';
btnGenerateCompiledReport.disabled = true;
btnGenerateCompiledReport.style.opacity = '0.6';
btnGenerateCompiledReport.style.cursor = 'not-allowed';
return;
}
try {
let url = `/api/observacoes/contar?crianca=${encodeURIComponent(crianca)}`;
if (dataInicio) url += `&dataInicio=${dataInicio}`;
if (dataFim) url += `&dataFim=${dataFim}`;
if (tag) url += `&tags=${encodeURIComponent(tag)}`;
if (ano) url += `&ano=${ano}`;
if (turma) url += `&turma=${encodeURIComponent(turma)}`;
const res = await fetch(url);
const data = await res.json();
const count = data.count || 0;
if (count === 0) {
emitirObsCountInfo.innerHTML = `⚠️ <span style="color: #ff6b6b; font-weight: 600;">Nenhuma observação</span> encontrada para esses filtros.`;
btnGenerateCompiledReport.disabled = true;
btnGenerateCompiledReport.style.opacity = '0.6';
btnGenerateCompiledReport.style.cursor = 'not-allowed';
} else {
emitirObsCountInfo.innerHTML = `✨ <strong>${count}</strong> observações selecionadas para compilação.`;
btnGenerateCompiledReport.disabled = false;
btnGenerateCompiledReport.style.opacity = '1';
btnGenerateCompiledReport.style.cursor = 'pointer';
}
} catch (err) {
console.error('Erro ao contar observações:', err);
}
}
// Gerar relatório consolidado com IA
if (btnGenerateCompiledReport) {
btnGenerateCompiledReport.addEventListener('click', async () => {
const crianca = emitirFilterCrianca.value;
const dataInicio = emitirFilterDataInicio.value;
const dataFim = emitirFilterDataFim.value;
const tags = emitirSelectedTags;
const modeloId = emitirFilterTemplate.value;
const ano = emitirFilterAno ? emitirFilterAno.value : '';
const turma = emitirFilterTurma ? emitirFilterTurma.value : '';
if (!crianca || !modeloId) return;
emitirReportPreviewArea.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 12px; margin-top: 50px;">
<div style="width: 32px; height: 32px; border: 3px solid var(--border-light); border-top-color: var(--brand-green); border-radius: 50%; animation: spin 1s linear infinite;"></div>
<div style="color: var(--text-secondary); font-size: 0.9rem; font-weight: 500;">Analisando observações e gerando relatório pedagógico...</div>
</div>
`;
btnGenerateCompiledReport.disabled = true;
btnGenerateCompiledReport.style.opacity = '0.6';
try {
const res = await fetch('/api/modelos/gerar-relatorio', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
crianca,
dataInicio,
dataFim,
tags: tags.length > 0 ? tags : undefined,
modeloId,
ano,
turma
})
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.error || 'Erro desconhecido');
}
const data = await res.json();
activeCompiledReport = data;
// Renderizar prévia do relatório em markdown
emitirReportPreviewArea.innerHTML = `
<div style="background: var(--bg-primary); padding: 12px; border-radius: 8px; margin-bottom: 16px; border: 1px solid var(--border-light); font-size: 0.85rem;">
<div><strong>Estudante:</strong> ${data.crianca}</div>
<div><strong>Período:</strong> ${data.periodo}</div>
<div><strong>Modelo Aplicado:</strong> ${data.templateNome}</div>
<div><strong>Quantidade de Observações Consolidadas:</strong> ${data.totalObservacoes}</div>
</div>
<div class="report-content-body" style="color: var(--text-primary);">
${marked.parse(data.report)}
</div>
`;
// Ativar botões de exportação
btnExportDocx.disabled = false;
btnExportDocx.style.cursor = 'pointer';
btnExportDocx.style.borderColor = 'var(--brand-green)';
btnExportDocx.style.color = 'var(--brand-green)';
btnExportPdf.disabled = false;
btnExportPdf.style.cursor = 'pointer';
btnExportPdf.style.borderColor = 'var(--brand-green)';
btnExportPdf.style.color = 'var(--brand-green)';
// Rolar suavemente até o relatório no mobile
if (window.innerWidth <= 768) {
const modalBody = emitirRelatorioModal.querySelector('.settings-modal-body');
if (modalBody) {
setTimeout(() => {
modalBody.scrollTo({
top: modalBody.scrollHeight,
behavior: 'smooth'
});
}, 150);
}
}
} catch (err) {
console.error('Erro ao gerar relatório compilado:', err);
emitirReportPreviewArea.innerHTML = `
<div style="color: #ff6b6b; text-align: center; margin-top: 50px; font-weight: 500;">
Erro ao gerar relatório: ${err.message}
</div>
`;
} finally {
btnGenerateCompiledReport.disabled = false;
btnGenerateCompiledReport.style.opacity = '1';
}
});
}
// Exportar para DOCX (Formato HTML-DOCX)
if (btnExportDocx) {
btnExportDocx.addEventListener('click', () => {
if (!activeCompiledReport) return;
const htmlContent = `
<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:office:word" xmlns="http://www.w3.org/TR/REC-html40">
<head>
<meta charset="utf-8">
<title>Relatório Pedagógico - ${activeCompiledReport.crianca}</title>
<style>
body { font-family: Arial, sans-serif; line-height: 1.5; color: #333; }
h1, h2, h3 { color: #1f2937; }
.header-info { background-color: #f3f4f6; padding: 12px; border-radius: 6px; margin-bottom: 20px; border: 1px solid #e5e7eb; }
.header-info div { margin-bottom: 4px; font-size: 11pt; }
</style>
</head>
<body>
<div class="header-info">
<div><b>Estudante:</b> ${activeCompiledReport.crianca}</div>
<div><b>Período:</b> ${activeCompiledReport.periodo}</div>
<div><b>Modelo:</b> ${activeCompiledReport.templateNome}</div>
<div><b>Quantidade de observações consolidadas:</b> ${activeCompiledReport.totalObservacoes}</div>
</div>
<hr/>
${marked.parse(activeCompiledReport.report)}
</body>
</html>
`;
const blob = new Blob(['\ufeff' + htmlContent], { type: 'application/msword' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `relatorio_${activeCompiledReport.crianca.replace(/\s+/g, '_')}_${Date.now()}.doc`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
}
// Exportar para PDF (Abre janela de impressão do navegador)
if (btnExportPdf) {
btnExportPdf.addEventListener('click', () => {
if (!activeCompiledReport) return;
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<html>
<head>
<title>Relatório Pedagógico - ${activeCompiledReport.crianca}</title>
<style>
body { font-family: 'Outfit', 'Inter', -apple-system, sans-serif; padding: 40px; color: #333; line-height: 1.6; }
h1, h2, h3 { color: #1e293b; margin-top: 24px; margin-bottom: 12px; }
h1 { border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; font-size: 22px; }
h2 { font-size: 18px; border-bottom: 1px solid #f1f5f9; padding-bottom: 6px; }
.meta-box { background: #f8fafc; padding: 16px; border-radius: 8px; margin-bottom: 24px; font-size: 14px; border: 1px solid #e2e8f0; }
.meta-item { margin-bottom: 6px; display: flex; }
.meta-label { font-weight: bold; color: #475569; width: 220px; }
p { margin-bottom: 14px; text-align: justify; }
ul, ol { margin-bottom: 14px; padding-left: 20px; }
li { margin-bottom: 6px; }
@media print {
body { padding: 0; }
button { display: none; }
}
</style>
</head>
<body>
<h1>Relatório Pedagógico de Desenvolvimento</h1>
<div class="meta-box">
<div class="meta-item"><span class="meta-label">Estudante:</span> <span>${activeCompiledReport.crianca}</span></div>
<div class="meta-item"><span class="meta-label">Período:</span> <span>${activeCompiledReport.periodo}</span></div>
<div class="meta-item"><span class="meta-label">Modelo Aplicado:</span> <span>${activeCompiledReport.templateNome}</span></div>
<div class="meta-item"><span class="meta-label">Quantidade de Observações:</span> <span>${activeCompiledReport.totalObservacoes}</span></div>
</div>
<div>${marked.parse(activeCompiledReport.report)}</div>
<script>
window.onload = function() {
window.print();
setTimeout(() => { window.close(); }, 500);
};
</script>
</body>
</html>
`);
printWindow.document.close();
});
}
// ============================================================
// ESTÚDIO MUSICAL (Criação de música cantada com MiniMax)
// ============================================================
const estudioMusicalModal = document.getElementById('estudioMusicalModal');
const btnCloseEstudioModal = document.getElementById('btnCloseEstudioModal');
const sidebarBtnEstudio = document.getElementById('sidebarBtnEstudio');
const barBtnEstudio = document.getElementById('barBtnEstudio');
const welcomeCardEstudio = document.getElementById('welcomeCardEstudio');
const btnGenerateStudioMusic = document.getElementById('btnGenerateStudioMusic');
const musicTemaInput = document.getElementById('musicTemaInput');
const musicGenerateLoader = document.getElementById('musicGenerateLoader');
const musicLoaderText = document.getElementById('musicLoaderText');
const musicResultArea = document.getElementById('musicResultArea');
const musicStudioAudioPlayer = document.getElementById('musicStudioAudioPlayer');
const musicStudioDownloadBtn = document.getElementById('musicStudioDownloadBtn');
const musicStudioLyricsArea = document.getElementById('musicStudioLyricsArea');
const btnCopyMusicLyrics = document.getElementById('btnCopyMusicLyrics');
const btnDownloadMusicLyrics = document.getElementById('btnDownloadMusicLyrics');
let selectedVoice = 'mulher';
let selectedRhythm = 'roda';
let selectedDuration = '30s';
// Toggle classes active para botões de opções de voz do Estúdio Musical
document.querySelectorAll('#estudioMusicalModal .music-option-grid .btn-music-option').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('#estudioMusicalModal .music-option-grid .btn-music-option').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedVoice = btn.dataset.voice;
});
});
document.querySelectorAll('.btn-music-rhythm').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.btn-music-rhythm').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedRhythm = btn.dataset.rhythm;
});
});
document.querySelectorAll('.btn-music-duration').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.btn-music-duration').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedDuration = btn.dataset.duration;
});
});
// Abrir e fechar modal
const openEstudioModal = () => {
estudioMusicalModal.style.display = 'flex';
musicTemaInput.value = '';
musicResultArea.style.display = 'none';
musicStudioAudioPlayer.src = '';
musicStudioLyricsArea.textContent = '';
if (musicGenerateLoader) musicGenerateLoader.style.display = 'none';
btnGenerateStudioMusic.disabled = false;
};
if (sidebarBtnEstudio) {
sidebarBtnEstudio.addEventListener('click', openEstudioModal);
}
if (barBtnEstudio) {
barBtnEstudio.addEventListener('click', openEstudioModal);
}
if (welcomeCardEstudio) {
welcomeCardEstudio.addEventListener('click', openEstudioModal);
}
if (btnCloseEstudioModal) {
btnCloseEstudioModal.addEventListener('click', () => {
estudioMusicalModal.style.display = 'none';
musicStudioAudioPlayer.pause();
});
}
// Fechar modal clicando fora
window.addEventListener('click', (e) => {
if (e.target === estudioMusicalModal) {
estudioMusicalModal.style.display = 'none';
musicStudioAudioPlayer.pause();
}
});
// Ação de gerar música no estúdio
if (btnGenerateStudioMusic) {
btnGenerateStudioMusic.addEventListener('click', async () => {
const tema = musicTemaInput.value.trim();
if (!tema) {
alert('Por favor, digite um tema para a música.');
return;
}
btnGenerateStudioMusic.disabled = true;
musicGenerateLoader.style.display = 'flex';
musicResultArea.style.display = 'none';
const statusInterval = setInterval(() => {
const statusMessages = [
'Compondo a letra pedagógica...',
'Ajustando a métrica e as rimas...',
'Gerando o arranjo musical...',
'Adicionando os vocais cantados...',
'Quase pronto, finalizando o áudio...'
];
const randomMsg = statusMessages[Math.floor(Math.random() * statusMessages.length)];
musicLoaderText.textContent = randomMsg;
}, 5000);
try {
const response = await fetch('/api/music/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
tema,
voz: selectedVoice,
ritmo: selectedRhythm,
duracao: selectedDuration
})
});
clearInterval(statusInterval);
if (!response.ok) {
const errData = await response.json();
throw new Error(errData.error || 'Erro na geração de áudio');
}
const data = await response.json();
// Exibir resultados
musicStudioLyricsArea.textContent = data.lyrics;
musicStudioAudioPlayer.src = data.audioUrl;
musicStudioDownloadBtn.href = data.audioUrl;
musicGenerateLoader.style.display = 'none';
musicResultArea.style.display = 'flex';
} catch (err) {
clearInterval(statusInterval);
console.error('Erro ao gerar música no estúdio:', err);
alert('Erro ao gerar música: ' + err.message);
musicGenerateLoader.style.display = 'none';
btnGenerateStudioMusic.disabled = false;
}
});
}
// Copiar letra
if (btnCopyMusicLyrics) {
btnCopyMusicLyrics.addEventListener('click', () => {
const lyrics = musicStudioLyricsArea.textContent;
if (lyrics) {
navigator.clipboard.writeText(lyrics)
.then(() => {
btnCopyMusicLyrics.textContent = '✅ Copiado!';
setTimeout(() => {
btnCopyMusicLyrics.textContent = '📋 Copiar Letra';
}, 2000);
})
.catch(err => {
console.error('Falha ao copiar:', err);
});
}
});
}
// Baixar letra em arquivo de texto (TXT)
if (btnDownloadMusicLyrics) {
btnDownloadMusicLyrics.addEventListener('click', () => {
const lyrics = musicStudioLyricsArea.textContent;
const tema = musicTemaInput.value.trim() || 'cantiga';
if (lyrics) {
const blob = new Blob([lyrics], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `letra_${tema.replace(/\s+/g, '_').toLowerCase()}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
});
}
// =====================================
// HISTÓRICO DO ESTÚDIO MUSICAL
// =====================================
const btnOpenEstudioHistory = document.getElementById('btnOpenEstudioHistory');
const estudioHistoryModal = document.getElementById('estudioHistoryModal');
const btnCloseEstudioHistory = document.getElementById('btnCloseEstudioHistory');
const estudioHistoryList = document.getElementById('estudioHistoryList');
if (btnOpenEstudioHistory) {
btnOpenEstudioHistory.addEventListener('click', async () => {
estudioHistoryModal.style.display = 'flex';
estudioHistoryList.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">Carregando histórico...</div>';
try {
const res = await fetch('/api/music/list');
const data = await res.json();
estudioHistoryList.innerHTML = '';
if (data.length === 0) {
estudioHistoryList.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">Nenhuma canção salva ainda no estúdio.</div>';
return;
}
data.forEach(item => {
const div = document.createElement('div');
div.style.background = 'var(--bg-tertiary)';
div.style.padding = '12px 15px';
div.style.borderRadius = '8px';
div.style.display = 'flex';
div.style.justifyContent = 'space-between';
div.style.alignItems = 'center';
div.innerHTML = `
<div style="flex: 1; margin-right: 15px;">
<div style="font-weight: 600; font-size: 0.95rem; color: var(--text-primary);">🎵 ${item.tema || 'Música'}</div>
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-top: 4px;">Voz: ${item.voz} | Ritmo: ${item.ritmo}</div>
<div style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 4px;">${new Date(item.created_at).toLocaleString()}</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn-ver-estudio btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.85rem; border-radius: 6px;">Ver/Ouvir</button>
<button class="btn-del-estudio btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.85rem; border-radius: 6px; color: #ef4444; border-color: rgba(239, 68, 68, 0.3);">🗑️</button>
</div>
`;
estudioHistoryList.appendChild(div);
});
document.querySelectorAll('.btn-ver-estudio').forEach(btn => {
btn.addEventListener('click', async (e) => {
const id = e.currentTarget.dataset.id;
try {
const res = await fetch(`/api/music/${id}`);
const itemData = await res.json();
musicStudioLyricsArea.textContent = itemData.letra;
musicStudioAudioPlayer.src = itemData.audio_url;
musicStudioDownloadBtn.href = itemData.audio_url;
musicResultArea.style.display = 'flex';
estudioHistoryModal.style.display = 'none';
} catch (err) { alert('Erro ao carregar música do estúdio.'); }
});
});
document.querySelectorAll('.btn-del-estudio').forEach(btn => {
btn.addEventListener('click', async (e) => {
if(!confirm('Tem certeza que deseja apagar esta canção?')) return;
const id = e.currentTarget.dataset.id;
try {
await fetch(`/api/music/${id}`, { method: 'DELETE' });
e.currentTarget.closest('div[style*="var(--bg-tertiary)"]').remove();
} catch (err) { alert('Erro ao deletar.'); }
});
});
} catch (err) {
estudioHistoryList.innerHTML = '<div style="color: #ef4444; padding: 20px;">Erro ao carregar histórico.</div>';
}
});
}
if (btnCloseEstudioHistory) {
btnCloseEstudioHistory.addEventListener('click', () => {
estudioHistoryModal.style.display = 'none';
});
}
// ============================================================
// FÁBRICA DE QUADRINHOS (Criação de histórias visuais pedagógicas)
// ============================================================
const fabricaQuadrinhosModal = document.getElementById('fabricaQuadrinhosModal');
const btnCloseComicsModal = document.getElementById('btnCloseComicsModal');
const barBtnComics = document.getElementById('barBtnComics');
const btnGenerateComics = document.getElementById('btnGenerateComics');
const comicsTemaInput = document.getElementById('comicsTemaInput');
const comicsGenerateLoader = document.getElementById('comicsGenerateLoader');
const comicsLoaderText = document.getElementById('comicsLoaderText');
const comicsLoaderProgress = document.getElementById('comicsLoaderProgress');
const comicsResultArea = document.getElementById('comicsResultArea');
const comicsResultTitle = document.getElementById('comicsResultTitle');
const comicsGridContainer = document.getElementById('comicsGridContainer');
const comicsCenarioSelect = document.getElementById('comicsCenarioSelect');
const comicsCenarioCustomInput = document.getElementById('comicsCenarioCustomInput');
// Export PDF / Vídeo
const btnExportComicsPDFPortrait = document.getElementById('btnExportComicsPDFPortrait');
const btnExportComicsPDFLandscape = document.getElementById('btnExportComicsPDFLandscape');
const btnCompileComicsVideo = document.getElementById('btnCompileComicsVideo');
const comicsVideoMusic = document.getElementById('comicsVideoMusic');
const comicsVideoDuration = document.getElementById('comicsVideoDuration');
const comicsVideoLoader = document.getElementById('comicsVideoLoader');
const comicsVideoResult = document.getElementById('comicsVideoResult');
const comicsVideoPlayer = document.getElementById('comicsVideoPlayer');
const comicsVideoDownloadBtn = document.getElementById('comicsVideoDownloadBtn');
const comicsProjectSelect = document.getElementById('comicsProjectSelect');
const btnNewComicsProject = document.getElementById('btnNewComicsProject');
const btnDeleteComicsProject = document.getElementById('btnDeleteComicsProject');
const comicsProjectTitleInput = document.getElementById('comicsProjectTitleInput');
const btnSaveComicsProject = document.getElementById('btnSaveComicsProject');
const btnSaveNewComicsProject = document.getElementById('btnSaveNewComicsProject');
let currentComicsProjectId = null;
let currentComicsCharDescEnglish = '';
let selectedComicsQty = 5;
let selectedComicsBubbles = true;
let selectedComicsRatio = '16:9';
let generatedPanelsData = [];
// Toggle ativo quantidade de quadros
document.querySelectorAll('.btn-comics-qty').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.btn-comics-qty').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedComicsQty = parseInt(btn.dataset.qty);
});
});
// Toggle ativo falas/diálogos
document.querySelectorAll('.btn-comics-bubbles').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.btn-comics-bubbles').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedComicsBubbles = btn.dataset.bubbles === 'true';
});
});
// Toggle ativo proporção visual
document.querySelectorAll('.btn-comics-ratio').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.btn-comics-ratio').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedComicsRatio = btn.dataset.ratio;
});
});
// Gerenciador dinâmico de personagens
const comicsCharListContainer = document.getElementById('comicsCharListContainer');
const btnAddHumanChar = document.getElementById('btnAddHumanChar');
const btnAddAnimalChar = document.getElementById('btnAddAnimalChar');
const addCharacterRow = (type = '', name = '', isAnimal = false) => {
const row = document.createElement('div');
row.className = 'comic-char-row';
row.style.cssText = 'display: flex; gap: 8px; align-items: center; background: var(--bg-primary); padding: 8px; border-radius: 8px; border: 1px solid var(--border-light);';
const emoji = document.createElement('span');
emoji.style.fontSize = '1.1rem';
emoji.textContent = isAnimal ? '🐾' : '🧒';
row.appendChild(emoji);
const typeInput = document.createElement('input');
typeInput.type = 'text';
typeInput.className = 'obs-input char-type-input';
typeInput.placeholder = isAnimal ? 'Tipo (ex: Gatinho, Leão)' : 'Tipo (ex: Menino, Menina)';
typeInput.value = type;
typeInput.style.cssText = 'flex: 1; padding: 6px 10px; font-size: 0.85rem; margin: 0;';
row.appendChild(typeInput);
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.className = 'obs-input char-name-input';
nameInput.placeholder = 'Nome';
nameInput.value = name;
nameInput.style.cssText = 'flex: 1; padding: 6px 10px; font-size: 0.85rem; margin: 0;';
row.appendChild(nameInput);
const deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.className = 'btn-delete-char';
deleteBtn.innerHTML = '&times;';
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' && !canvasModeActive
? `<div style="width: 100%; margin-bottom: 20px; border-radius: 8px; overflow: hidden; border: 2px solid rgba(0,0,0,0.1); text-align: center; background: rgba(0,0,0,0.02);"><img src="${cartazPreviewImage.src}" style="max-width: 100%; max-height: 350px; object-fit: contain; display: inline-block;"></div>`
: '';
let printContent = '';
if (canvasModeActive) {
// Clone printable area and cleanup handles / active state borders for print output
const tempContainer = document.createElement('div');
tempContainer.innerHTML = cartazPrintableArea.innerHTML;
tempContainer.querySelectorAll('.canvas-resize-handle').forEach(h => h.remove());
tempContainer.querySelectorAll('.canvas-item').forEach(item => {
item.classList.remove('active-item');
item.removeAttribute('contenteditable');
// We can remove border style only if it was a dashed editing border
if (item.style.border.includes('dashed')) {
item.style.border = 'none';
}
});
printContent = tempContainer.innerHTML;
} else {
printContent = `
<h1>${title}</h1>
${imgContainerHtml}
<div class="content-area">
${cartazPreviewContent.innerHTML}
</div>
`;
}
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<html>
<head>
<title>${title} - PedaGog</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&family=Caveat:wght@400;700&family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
<style>
@page {
size: ${tamanho} ${orientacao === 'paisagem' ? 'landscape' : 'portrait'};
margin: 10mm;
}
body {
font-family: ${font};
margin: 0;
padding: 20px;
background-color: #f7fafc;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.cartaz-frame {
background-color: ${bg};
color: ${text};
padding: 40px;
border-radius: 16px;
width: 100%;
max-width: ${orientacao === 'paisagem' ? '1200px' : '800px'};
aspect-ratio: ${orientacao === 'paisagem' ? '1.414 / 1' : '1 / 1.414'};
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
box-sizing: border-box;
border: ${border === 'none' ? 'none' : `${border} ${text}`};
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
container-type: inline-size;
}
h1 {
text-align: center;
margin-top: 0;
font-size: 3.5rem;
border-bottom: 3px solid ${text};
padding-bottom: 16px;
margin-bottom: 30px;
}
.content-area {
font-size: 1.5rem;
line-height: 1.6;
display: flex;
flex-direction: ${orientacao === 'paisagem' ? 'row' : 'column'};
flex-wrap: wrap;
gap: 20px;
justify-content: center;
}
/* Suporte para cards na content-area */
.content-area > div, .content-area > p, .content-area > ul {
box-sizing: border-box;
background: rgba(255, 255, 255, 0.3);
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
width: ${orientacao === 'paisagem' ? 'calc(50% - 20px)' : '100%'};
flex: ${orientacao === 'paisagem' ? '1 1 calc(50% - 20px)' : '0 0 auto'};
margin: 0;
}
ul {
margin-left: 20px;
}
li {
margin-bottom: 12px;
}
/* Estilos específicos do Canvas no PDF/Print */
.canvas-item {
position: absolute !important;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.3);
border-radius: 8px;
padding: 10px;
border: none !important;
box-shadow: none !important;
}
@media print {
body { background: transparent; padding: 0; display: block; }
.cartaz-frame {
box-shadow: none;
border-radius: 0;
max-width: 100%;
width: 100%;
height: auto;
min-height: 100%;
padding: 30px;
}
button { display: none; }
}
</style>
</head>
<body>
<div class="cartaz-frame">
${printContent}
</div>
<script>
window.onload = function() {
window.print();
}
<\/script>
</body>
</html>
`);
printWindow.document.close();
});
}
// Load History list
async function loadCartazesHistory() {
if (!cartazesHistoryContainer) return;
cartazesHistoryContainer.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">Carregando histórico...</div>';
try {
const res = await fetch('/api/cartazes/list', {
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
});
const data = await res.json();
cartazesHistoryContainer.innerHTML = '';
if (!data || data.length === 0) {
cartazesNoData.style.display = 'block';
return;
}
cartazesNoData.style.display = 'none';
data.forEach(item => {
const card = document.createElement('div');
card.style.background = 'var(--bg-tertiary)';
card.style.border = '1px solid var(--border-light)';
card.style.borderRadius = '10px';
card.style.padding = '16px';
card.style.display = 'flex';
card.style.flexDirection = 'column';
card.style.justifyContent = 'space-between';
card.style.gap = '12px';
card.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
const thumbnail = item.imagemUrl
? `<div style="height: 120px; overflow:hidden; border-radius: 6px; border: 1px solid var(--border-light); margin-bottom: 6px;">
<img src="${item.imagemUrl}" style="width:100%; height:100%; object-fit:cover;">
</div>`
: `<div style="height: 120px; border-radius: 6px; background: ${item.corFundo || '#fff9eb'}; color: ${item.corTexto || '#2d3748'}; display:flex; align-items:center; justify-content:center; border: 1px solid var(--border-light); margin-bottom: 6px; font-weight: bold; font-size: 0.8rem; overflow: hidden; padding: 10px; text-align:center;">
${item.titulo}
</div>`;
card.innerHTML = `
<div>
${thumbnail}
<h5 style="margin: 0; font-size: 1rem; color: var(--text-primary); font-family: 'Outfit', sans-serif;">${item.titulo}</h5>
<p style="margin: 4px 0 0 0; font-size: 0.8rem; color: var(--text-secondary);">Foco: ${item.tema}</p>
<span style="font-size: 0.75rem; color: var(--text-muted); display: block; margin-top: 6px;">Layout: ${item.layout.toUpperCase()}</span>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end; border-top: 1px solid var(--border-light); padding-top: 10px;">
<button class="btn-ver-cartaz btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.8rem; border-radius: 6px; cursor: pointer; background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.3); color: #60a5fa;">Ver / Editar</button>
<button class="btn-del-cartaz btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.8rem; border-radius: 6px; cursor: pointer; color: #ef4444; border-color: rgba(239, 68, 68, 0.3);">🗑️</button>
</div>
`;
cartazesHistoryContainer.appendChild(card);
});
// Bind Ver/Editar Buttons
document.querySelectorAll('.btn-ver-cartaz').forEach(btn => {
btn.addEventListener('click', async (e) => {
const id = e.currentTarget.dataset.id;
try {
const r = await fetch(`/api/cartazes/${id}`, {
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
});
const poster = await r.json();
// Set Inputs
cartazTituloInput.value = poster.titulo;
cartazTemaInput.value = poster.tema;
cartazLayoutSelect.value = poster.layout;
cartazBgColor.value = poster.corFundo;
cartazTextColor.value = poster.corTexto;
cartazHtmlEditor.value = poster.conteudo;
cartazPreviewContent.innerHTML = poster.conteudo;
if (poster.imagemUrl) {
cartazPreviewImage.src = poster.imagemUrl;
cartazPreviewImageContainer.style.display = 'block';
} else {
cartazPreviewImageContainer.style.display = 'none';
}
updateLivePreview();
switchCartazTab('criar');
} catch (err) {
showToast('Erro ao carregar cartaz.', 'error');
}
});
});
// Bind Delete Buttons
document.querySelectorAll('.btn-del-cartaz').forEach(btn => {
btn.addEventListener('click', async (e) => {
if (!confirm('Tem certeza que deseja apagar este cartaz?')) return;
const id = e.currentTarget.dataset.id;
try {
const r = await fetch(`/api/cartazes/${id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
});
if (r.ok) {
showToast('Cartaz deletado com sucesso.', 'success');
loadCartazesHistory();
} else {
showToast('Erro ao deletar cartaz.', 'error');
}
} catch (err) {
showToast('Erro de conexão ao deletar cartaz.', 'error');
}
});
});
} catch (err) {
console.error(err);
cartazesHistoryContainer.innerHTML = '<div style="color: #ef4444; padding: 20px;">Erro ao carregar histórico de cartazes.</div>';
}
}
// --- MODO DESIGN CANVAS (EDITOR INTERATIVO DE CARTAZES) ---
const btnCanvasModeToggle = document.getElementById('btnCanvasModeToggle');
const canvasControlPanel = document.getElementById('canvasControlPanel');
const btnCanvasAddText = document.getElementById('btnCanvasAddText');
const btnCanvasAddCard = document.getElementById('btnCanvasAddCard');
const canvasFontFamily = document.getElementById('canvasFontFamily');
const canvasFontSize = document.getElementById('canvasFontSize');
const canvasTextColor = document.getElementById('canvasTextColor');
const canvasBgColorInput = document.getElementById('canvasBgColor');
const btnCanvasZIndexUp = document.getElementById('btnCanvasZIndexUp');
const btnCanvasZIndexDown = document.getElementById('btnCanvasZIndexDown');
const btnCanvasDelete = document.getElementById('btnCanvasDelete');
let canvasModeActive = false;
let selectedCanvasItem = null;
let isDraggingCanvas = false;
let isResizingCanvas = false;
let dragStartX, dragStartY;
let dragStartLeft, dragStartTop;
let dragStartWidth, dragStartHeight;
function enterCanvasMode() {
canvasModeActive = true;
if (btnCanvasModeToggle) {
btnCanvasModeToggle.textContent = '💾 Salvar Canvas';
btnCanvasModeToggle.style.background = '#10a37f';
btnCanvasModeToggle.style.color = 'white';
}
if (canvasControlPanel) canvasControlPanel.style.display = 'flex';
cartazPrintableArea.style.position = 'relative';
const rectParent = cartazPrintableArea.getBoundingClientRect();
const items = [];
if (cartazPreviewTitle) items.push(cartazPreviewTitle);
if (cartazPreviewImageContainer && cartazPreviewImageContainer.style.display !== 'none') {
items.push(cartazPreviewImageContainer);
}
const cards = Array.from(cartazPreviewContent.children);
cards.forEach(card => {
items.push(card);
});
const clientRects = items.map(item => item.getBoundingClientRect());
items.forEach((item, index) => {
const rect = clientRects[index];
const leftPercent = ((rect.left - rectParent.left) / rectParent.width) * 100;
const topPercent = ((rect.top - rectParent.top) / rectParent.height) * 100;
const widthPercent = (rect.width / rectParent.width) * 100;
const heightPercent = (rect.height / rectParent.height) * 100;
item.classList.add('canvas-item');
item.style.position = 'absolute';
item.style.left = `${leftPercent}%`;
item.style.top = `${topPercent}%`;
item.style.width = `${widthPercent}%`;
item.style.height = `${heightPercent}%`;
item.style.margin = '0';
item.setAttribute('contenteditable', 'true');
if (!item.querySelector('.canvas-resize-handle')) {
const handle = document.createElement('div');
handle.className = 'canvas-resize-handle';
item.appendChild(handle);
}
if (item.parentNode === cartazPreviewContent) {
cartazPrintableArea.appendChild(item);
}
setupCanvasItemEvents(item);
});
cartazPreviewContent.style.display = 'none';
showToast('Modo Design Canvas ativado! Arraste, redimensione e clique duas vezes para editar o texto.', 'info');
}
function exitCanvasMode() {
canvasModeActive = false;
if (btnCanvasModeToggle) {
btnCanvasModeToggle.textContent = '🎨 Design Canvas';
btnCanvasModeToggle.style.background = 'transparent';
btnCanvasModeToggle.style.color = 'var(--text-secondary)';
}
if (canvasControlPanel) canvasControlPanel.style.display = 'none';
if (selectedCanvasItem) {
selectedCanvasItem.classList.remove('active-item');
selectedCanvasItem = null;
}
const items = cartazPrintableArea.querySelectorAll('.canvas-item');
items.forEach(item => {
item.removeAttribute('contenteditable');
});
showToast('Layout do Canvas salvo com sucesso!', 'success');
}
function setupCanvasItemEvents(item) {
item.addEventListener('click', (e) => {
if (!canvasModeActive) return;
e.stopPropagation();
if (selectedCanvasItem) {
selectedCanvasItem.classList.remove('active-item');
}
selectedCanvasItem = item;
item.classList.add('active-item');
updateToolbarForSelected();
});
item.addEventListener('mousedown', (e) => {
if (!canvasModeActive) return;
if (e.target.classList.contains('canvas-resize-handle')) return;
if (document.activeElement === item && e.target !== item && !e.target.classList.contains('canvas-item')) {
return;
}
e.preventDefault();
selectedCanvasItem = item;
document.querySelectorAll('.canvas-item').forEach(el => el.classList.remove('active-item'));
item.classList.add('active-item');
updateToolbarForSelected();
isDraggingCanvas = true;
dragStartX = e.clientX;
dragStartY = e.clientY;
const parentRect = cartazPrintableArea.getBoundingClientRect();
const itemRect = item.getBoundingClientRect();
dragStartLeft = ((itemRect.left - parentRect.left) / parentRect.width) * 100;
dragStartTop = ((itemRect.top - parentRect.top) / parentRect.height) * 100;
document.addEventListener('mousemove', handleCanvasDrag);
document.addEventListener('mouseup', stopCanvasDrag);
});
item.addEventListener('touchstart', (e) => {
if (!canvasModeActive) return;
if (e.target.classList.contains('canvas-resize-handle')) return;
if (document.activeElement === item && e.target !== item) return;
selectedCanvasItem = item;
document.querySelectorAll('.canvas-item').forEach(el => el.classList.remove('active-item'));
item.classList.add('active-item');
updateToolbarForSelected();
isDraggingCanvas = true;
const touch = e.touches[0];
dragStartX = touch.clientX;
dragStartY = touch.clientY;
const parentRect = cartazPrintableArea.getBoundingClientRect();
const itemRect = item.getBoundingClientRect();
dragStartLeft = ((itemRect.left - parentRect.left) / parentRect.width) * 100;
dragStartTop = ((itemRect.top - parentRect.top) / parentRect.height) * 100;
document.addEventListener('touchmove', handleCanvasTouchDrag, { passive: false });
document.addEventListener('touchend', stopCanvasDrag);
});
const handle = item.querySelector('.canvas-resize-handle');
if (handle) {
handle.addEventListener('mousedown', (e) => {
if (!canvasModeActive) return;
e.stopPropagation();
e.preventDefault();
isResizingCanvas = true;
dragStartX = e.clientX;
dragStartY = e.clientY;
const parentRect = cartazPrintableArea.getBoundingClientRect();
const itemRect = item.getBoundingClientRect();
dragStartWidth = (itemRect.width / parentRect.width) * 100;
dragStartHeight = (itemRect.height / parentRect.height) * 100;
document.addEventListener('mousemove', handleCanvasResize);
document.addEventListener('mouseup', stopCanvasResize);
});
handle.addEventListener('touchstart', (e) => {
if (!canvasModeActive) return;
e.stopPropagation();
e.preventDefault();
isResizingCanvas = true;
const touch = e.touches[0];
dragStartX = touch.clientX;
dragStartY = touch.clientY;
const parentRect = cartazPrintableArea.getBoundingClientRect();
const itemRect = item.getBoundingClientRect();
dragStartWidth = (itemRect.width / parentRect.width) * 100;
dragStartHeight = (itemRect.height / parentRect.height) * 100;
document.addEventListener('touchmove', handleCanvasTouchResize, { passive: false });
document.addEventListener('touchend', stopCanvasResize);
});
}
}
function handleCanvasDrag(e) {
if (!isDraggingCanvas || !selectedCanvasItem) return;
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
const parentRect = cartazPrintableArea.getBoundingClientRect();
const dxPercent = (dx / parentRect.width) * 100;
const dyPercent = (dy / parentRect.height) * 100;
let newLeft = dragStartLeft + dxPercent;
let newTop = dragStartTop + dyPercent;
const itemRect = selectedCanvasItem.getBoundingClientRect();
const itemWPercent = (itemRect.width / parentRect.width) * 100;
const itemHPercent = (itemRect.height / parentRect.height) * 100;
if (newLeft < 0) newLeft = 0;
if (newTop < 0) newTop = 0;
if (newLeft + itemWPercent > 100) newLeft = 100 - itemWPercent;
if (newTop + itemHPercent > 100) newTop = 100 - itemHPercent;
selectedCanvasItem.style.left = `${newLeft}%`;
selectedCanvasItem.style.top = `${newTop}%`;
}
function handleCanvasTouchDrag(e) {
if (!isDraggingCanvas || !selectedCanvasItem) return;
e.preventDefault();
const touch = e.touches[0];
const dx = touch.clientX - dragStartX;
const dy = touch.clientY - dragStartY;
const parentRect = cartazPrintableArea.getBoundingClientRect();
const dxPercent = (dx / parentRect.width) * 100;
const dyPercent = (dy / parentRect.height) * 100;
let newLeft = dragStartLeft + dxPercent;
let newTop = dragStartTop + dyPercent;
const itemRect = selectedCanvasItem.getBoundingClientRect();
const itemWPercent = (itemRect.width / parentRect.width) * 100;
const itemHPercent = (itemRect.height / parentRect.height) * 100;
if (newLeft < 0) newLeft = 0;
if (newTop < 0) newTop = 0;
if (newLeft + itemWPercent > 100) newLeft = 100 - itemWPercent;
if (newTop + itemHPercent > 100) newTop = 100 - itemHPercent;
selectedCanvasItem.style.left = `${newLeft}%`;
selectedCanvasItem.style.top = `${newTop}%`;
}
function stopCanvasDrag() {
isDraggingCanvas = false;
document.removeEventListener('mousemove', handleCanvasDrag);
document.removeEventListener('mouseup', stopCanvasDrag);
document.removeEventListener('touchmove', handleCanvasTouchDrag);
document.removeEventListener('touchend', stopCanvasDrag);
}
function handleCanvasResize(e) {
if (!isResizingCanvas || !selectedCanvasItem) return;
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
const parentRect = cartazPrintableArea.getBoundingClientRect();
const dxPercent = (dx / parentRect.width) * 100;
const dyPercent = (dy / parentRect.height) * 100;
let newWidth = dragStartWidth + dxPercent;
let newHeight = dragStartHeight + dyPercent;
if (newWidth < 10) newWidth = 10;
if (newHeight < 5) newHeight = 5;
const itemLeft = parseFloat(selectedCanvasItem.style.left) || 0;
const itemTop = parseFloat(selectedCanvasItem.style.top) || 0;
if (itemLeft + newWidth > 100) newWidth = 100 - itemLeft;
if (itemTop + newHeight > 100) newHeight = 100 - itemTop;
selectedCanvasItem.style.width = `${newWidth}%`;
selectedCanvasItem.style.height = `${newHeight}%`;
}
function handleCanvasTouchResize(e) {
if (!isResizingCanvas || !selectedCanvasItem) return;
e.preventDefault();
const touch = e.touches[0];
const dx = touch.clientX - dragStartX;
const dy = touch.clientY - dragStartY;
const parentRect = cartazPrintableArea.getBoundingClientRect();
const dxPercent = (dx / parentRect.width) * 100;
const dyPercent = (dy / parentRect.height) * 100;
let newWidth = dragStartWidth + dxPercent;
let newHeight = dragStartHeight + dyPercent;
if (newWidth < 10) newWidth = 10;
if (newHeight < 5) newHeight = 5;
const itemLeft = parseFloat(selectedCanvasItem.style.left) || 0;
const itemTop = parseFloat(selectedCanvasItem.style.top) || 0;
if (itemLeft + newWidth > 100) newWidth = 100 - itemLeft;
if (itemTop + newHeight > 100) newHeight = 100 - itemTop;
selectedCanvasItem.style.width = `${newWidth}%`;
selectedCanvasItem.style.height = `${newHeight}%`;
}
function stopCanvasResize() {
isResizingCanvas = false;
document.removeEventListener('mousemove', handleCanvasResize);
document.removeEventListener('mouseup', stopCanvasResize);
document.removeEventListener('touchmove', handleCanvasTouchResize);
document.removeEventListener('touchend', stopCanvasResize);
}
function rgbToHex(rgb) {
if (!rgb || rgb === 'transparent') return '#ffffff';
if (rgb.startsWith('#')) return rgb;
const match = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);
if (!match) return '#ffffff';
const r = parseInt(match[1]);
const g = parseInt(match[2]);
const b = parseInt(match[3]);
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
function updateToolbarForSelected() {
if (!selectedCanvasItem) return;
const ff = window.getComputedStyle(selectedCanvasItem).fontFamily.replace(/['"]/g, '').split(',')[0].trim();
if (canvasFontFamily) canvasFontFamily.value = ff;
const fs = parseFloat(window.getComputedStyle(selectedCanvasItem).fontSize);
if (canvasFontSize) canvasFontSize.value = Math.round(fs);
const col = rgbToHex(window.getComputedStyle(selectedCanvasItem).color);
if (canvasTextColor) canvasTextColor.value = col;
const bg = rgbToHex(window.getComputedStyle(selectedCanvasItem).backgroundColor);
if (canvasBgColorInput) canvasBgColorInput.value = bg;
}
if (canvasFontFamily) {
canvasFontFamily.addEventListener('change', () => {
if (selectedCanvasItem) selectedCanvasItem.style.fontFamily = canvasFontFamily.value;
});
}
if (canvasFontSize) {
canvasFontSize.addEventListener('input', () => {
if (selectedCanvasItem) selectedCanvasItem.style.fontSize = `${canvasFontSize.value}px`;
});
}
if (canvasTextColor) {
canvasTextColor.addEventListener('input', () => {
if (selectedCanvasItem) selectedCanvasItem.style.color = canvasTextColor.value;
});
}
if (canvasBgColorInput) {
canvasBgColorInput.addEventListener('input', () => {
if (selectedCanvasItem) selectedCanvasItem.style.backgroundColor = canvasBgColorInput.value;
});
}
if (btnCanvasZIndexUp) {
btnCanvasZIndexUp.addEventListener('click', () => {
if (selectedCanvasItem) {
const currentZ = parseInt(selectedCanvasItem.style.zIndex) || 1;
selectedCanvasItem.style.zIndex = currentZ + 1;
}
});
}
if (btnCanvasZIndexDown) {
btnCanvasZIndexDown.addEventListener('click', () => {
if (selectedCanvasItem) {
const currentZ = parseInt(selectedCanvasItem.style.zIndex) || 1;
selectedCanvasItem.style.zIndex = Math.max(1, currentZ - 1);
}
});
}
if (btnCanvasDelete) {
btnCanvasDelete.addEventListener('click', () => {
if (selectedCanvasItem) {
selectedCanvasItem.remove();
selectedCanvasItem = null;
}
});
}
if (btnCanvasAddText) {
btnCanvasAddText.addEventListener('click', () => {
if (!canvasModeActive) return;
const textItem = document.createElement('div');
textItem.className = 'canvas-item';
textItem.innerHTML = 'Novo Texto (Clique duplo para editar)';
textItem.style.left = '10%';
textItem.style.top = '15%';
textItem.style.width = '50%';
textItem.style.height = '8%';
textItem.style.fontSize = '20px';
textItem.style.color = cartazTextColor.value || '#2d3748';
textItem.style.fontFamily = cartazFontSelect.value || 'Fredoka';
textItem.setAttribute('contenteditable', 'true');
const handle = document.createElement('div');
handle.className = 'canvas-resize-handle';
textItem.appendChild(handle);
cartazPrintableArea.appendChild(textItem);
setupCanvasItemEvents(textItem);
textItem.click();
});
}
if (btnCanvasAddCard) {
btnCanvasAddCard.addEventListener('click', () => {
if (!canvasModeActive) return;
const cardItem = document.createElement('div');
cardItem.className = 'canvas-item';
cardItem.style.background = 'rgba(255, 255, 255, 0.4)';
cardItem.style.border = '2px dashed ' + (cartazTextColor.value || '#2d3748');
cardItem.style.borderRadius = '12px';
cardItem.style.left = '20%';
cardItem.style.top = '25%';
cardItem.style.width = '45%';
cardItem.style.height = '20%';
cardItem.style.padding = '15px';
cardItem.innerHTML = '<h3>Novo Card</h3><p>Edite este texto...</p>';
cardItem.style.fontFamily = cartazFontSelect.value || 'Fredoka';
cardItem.setAttribute('contenteditable', 'true');
const handle = document.createElement('div');
handle.className = 'canvas-resize-handle';
cardItem.appendChild(handle);
cartazPrintableArea.appendChild(cardItem);
setupCanvasItemEvents(cardItem);
cardItem.click();
});
}
if (btnCanvasModeToggle) {
btnCanvasModeToggle.addEventListener('click', () => {
if (canvasModeActive) {
exitCanvasMode();
} else {
if (cartazEditArea && cartazEditArea.style.display !== 'none') {
btnEditCartazToggle.click();
}
enterCanvasMode();
}
});
}
if (cartazPrintableArea) {
cartazPrintableArea.addEventListener('click', (e) => {
if (!canvasModeActive) return;
if (e.target === cartazPrintableArea) {
if (selectedCanvasItem) {
selectedCanvasItem.classList.remove('active-item');
selectedCanvasItem = null;
}
}
});
}
});