🚀 Auto-deploy: Camila atualizado em 06/06/2026 21:07:19

This commit is contained in:
2026-06-06 21:07:19 +00:00
parent 8ce48af127
commit 852713db00
3 changed files with 780 additions and 1 deletions
+405
View File
@@ -6785,5 +6785,410 @@ document.addEventListener('DOMContentLoaded', () => {
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';
cartazPrintableArea.style.backgroundColor = bg;
cartazPrintableArea.style.color = text;
cartazPrintableArea.style.fontFamily = font;
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].forEach(elem => {
if (elem) elem.addEventListener('input', 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();
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
})
});
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 imgContainerHtml = cartazPreviewImageContainer.style.display === 'block'
? `<div style="width: 100%; margin-bottom: 20px; border-radius: 8px; overflow: hidden; max-height: 350px; border: 2px solid rgba(0,0,0,0.1);"><img src="${cartazPreviewImage.src}" style="width: 100%; height: 100%; object-fit: cover;"></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>
body {
font-family: ${font};
margin: 0;
padding: 40px;
background-color: #f7fafc;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.cartaz-frame {
background-color: ${bg};
color: ${text};
padding: 50px;
border-radius: 16px;
width: 100%;
max-width: 800px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
box-sizing: border-box;
border: ${border === 'none' ? 'none' : `${border} ${text}`};
}
h1 {
text-align: center;
margin-top: 0;
font-size: 3rem;
border-bottom: 3px solid ${text};
padding-bottom: 16px;
margin-bottom: 30px;
}
.content-area {
font-size: 1.4rem;
line-height: 1.8;
}
ul {
margin-left: 20px;
}
li {
margin-bottom: 12px;
}
@media print {
body { background: transparent; padding: 0; }
.cartaz-frame { box-shadow: none; border-radius: 0; max-width: 100%; width: 100%; height: 100%; padding: 30px; }
button { display: none; }
}
</style>
</head>
<body>
<div class="cartaz-frame">
<h1>${title}</h1>
${imgContainerHtml}
<div class="content-area">
${cartazPreviewContent.innerHTML}
</div>
</div>
<script>
window.onload = function() {
window.print();
}
<\/script>
</body>
</html>
`);
printWindow.document.close();
});
}
// Load History list
async function loadCartazesHistory() {
if (!cartazesHistoryContainer) return;
cartazesHistoryContainer.innerHTML = '<div style="text-align: center; color: var(--text-secondary); padding: 20px;">Carregando histórico...</div>';
try {
const res = await fetch('/api/cartazes/list', {
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
});
const data = await res.json();
cartazesHistoryContainer.innerHTML = '';
if (!data || data.length === 0) {
cartazesNoData.style.display = 'block';
return;
}
cartazesNoData.style.display = 'none';
data.forEach(item => {
const card = document.createElement('div');
card.style.background = 'var(--bg-tertiary)';
card.style.border = '1px solid var(--border-light)';
card.style.borderRadius = '10px';
card.style.padding = '16px';
card.style.display = 'flex';
card.style.flexDirection = 'column';
card.style.justifyContent = 'space-between';
card.style.gap = '12px';
card.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
const thumbnail = item.imagemUrl
? `<div style="height: 120px; overflow:hidden; border-radius: 6px; border: 1px solid var(--border-light); margin-bottom: 6px;">
<img src="${item.imagemUrl}" style="width:100%; height:100%; object-fit:cover;">
</div>`
: `<div style="height: 120px; border-radius: 6px; background: ${item.corFundo || '#fff9eb'}; color: ${item.corTexto || '#2d3748'}; display:flex; align-items:center; justify-content:center; border: 1px solid var(--border-light); margin-bottom: 6px; font-weight: bold; font-size: 0.8rem; overflow: hidden; padding: 10px; text-align:center;">
${item.titulo}
</div>`;
card.innerHTML = `
<div>
${thumbnail}
<h5 style="margin: 0; font-size: 1rem; color: var(--text-primary); font-family: 'Outfit', sans-serif;">${item.titulo}</h5>
<p style="margin: 4px 0 0 0; font-size: 0.8rem; color: var(--text-secondary);">Foco: ${item.tema}</p>
<span style="font-size: 0.75rem; color: var(--text-muted); display: block; margin-top: 6px;">Layout: ${item.layout.toUpperCase()}</span>
</div>
<div style="display: flex; gap: 8px; justify-content: flex-end; border-top: 1px solid var(--border-light); padding-top: 10px;">
<button class="btn-ver-cartaz btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.8rem; border-radius: 6px; cursor: pointer; background: rgba(59, 130, 246, 0.1); border-color: rgba(59, 130, 246, 0.3); color: #60a5fa;">Ver / Editar</button>
<button class="btn-del-cartaz btn-secondary" data-id="${item.id}" style="padding: 6px 12px; font-size: 0.8rem; border-radius: 6px; cursor: pointer; color: #ef4444; border-color: rgba(239, 68, 68, 0.3);">🗑</button>
</div>
`;
cartazesHistoryContainer.appendChild(card);
});
// Bind Ver/Editar Buttons
document.querySelectorAll('.btn-ver-cartaz').forEach(btn => {
btn.addEventListener('click', async (e) => {
const id = e.target.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.target.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>';
}
}
});
+160 -1
View File
@@ -16,7 +16,7 @@
<!-- Google Fonts Outfit & Inter -->
<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=Inter:wght@300;400;500;600&family=Outfit:wght@400;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Outfit:wght@400;600;700&family=Fredoka:wght@400;600;700&family=Caveat:wght@400;700&family=Playfair+Display:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
<!-- Highlight.js para destaque de sintaxe em códigos -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<link rel="stylesheet" href="style.css">
@@ -278,6 +278,9 @@
<button type="button" class="btn-prompt-mode mode-text" id="barBtnHistoria" title="Escrever histórias, relatórios ou conversar">
<span class="mode-icon">✍️</span> Inventando Historias
</button>
<button type="button" class="btn-prompt-mode mode-comics" id="barBtnCartazes" style="border-color: rgba(16, 185, 129, 0.4); color: #34d399; background: rgba(16, 185, 129, 0.05);" title="Criar e armazenar cartazes para sala de aula">
<span class="mode-icon">🖼️</span> Criar Cartazes
</button>
</div>
<form id="chatForm">
<div class="input-box">
@@ -1683,6 +1686,162 @@
</div>
</div>
<!-- ========================================== -->
<!-- MODAL: CRIAR CARTAZES -->
<!-- ========================================== -->
<div id="cartazesModal" class="settings-modal" style="display: none;">
<div class="settings-modal-content" style="max-width: 1100px; width: 95%; max-height: 95vh; display: flex; flex-direction: column;">
<div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light);">
<h3 style="display: flex; align-items: center; gap: 8px; font-family: 'Outfit', sans-serif;">🖼️ Estúdio de Cartazes PedaGog</h3>
<div style="display: flex; gap: 10px;">
<button id="btnCloseCartazesModal" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem;">&times;</button>
</div>
</div>
<!-- Abas do Modal -->
<div style="padding: 12px 20px; display: flex; gap: 15px; border-bottom: 1px solid var(--border-light); background: var(--bg-primary);">
<button id="tabCartazesCriar" class="tab-btn active" style="background: transparent; border: none; color: var(--text-primary); font-weight: 600; padding: 6px 12px; border-radius: 6px; cursor: pointer;">✨ Criar Cartaz</button>
<button id="tabCartazesHistorico" class="tab-btn" style="background: transparent; border: none; color: var(--text-secondary); font-weight: 600; padding: 6px 12px; border-radius: 6px; cursor: pointer;">🕒 Seus Cartazes Salvos</button>
</div>
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; flex: 1; display: flex; flex-direction: column;">
<!-- Conteúdo 1: Criador -->
<div id="panelCartazesCriar" style="display: flex; flex-direction: row; gap: 24px; height: 100%; flex-wrap: wrap;">
<!-- Lado Esquerdo: Configurações -->
<div style="flex: 1; min-width: 320px; display: flex; flex-direction: column; gap: 16px;">
<h4 style="margin: 0; color: var(--text-primary); border-bottom: 1px dashed var(--border-light); padding-bottom: 8px;">1. Definições do Cartaz</h4>
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.8rem; font-weight: 600; text-transform: uppercase;">Título do Cartaz</label>
<input type="text" id="cartazTituloInput" placeholder="Ex: Regras da Nossa Sala" class="obs-input" style="width: 100%; margin-top: 4px; padding: 10px;">
</div>
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.8rem; font-weight: 600; text-transform: uppercase;">Tema / Objetivo Pedagógico</label>
<input type="text" id="cartazTemaInput" placeholder="Ex: Combinados positivos de convivência" class="obs-input" style="width: 100%; margin-top: 4px; padding: 10px;">
</div>
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.8rem; font-weight: 600; text-transform: uppercase;">Layout Visual</label>
<select id="cartazLayoutSelect" class="obs-select" style="width: 100%; margin-top: 4px;">
<option value="regras">📜 Combinados / Regras (Cards Individuais)</option>
<option value="mural">📌 Mural de Avisos (Quadro Informativo)</option>
<option value="calendario">📅 Calendário e Clima (Rotina Semanal)</option>
<option value="livre">🎨 Livre / Cartaz Temático Customizado</option>
</select>
</div>
<h4 style="margin: 0; color: var(--text-primary); border-bottom: 1px dashed var(--border-light); padding-bottom: 8px; margin-top: 8px;">2. Estilo e Cores</h4>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<div class="settings-group" style="flex: 1; min-width: 140px;">
<label style="color: var(--text-secondary); font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Cor de Fundo</label>
<input type="color" id="cartazBgColor" value="#fff9eb" style="width: 100%; height: 38px; padding: 2px; border: 1px solid var(--border-light); border-radius: 6px; cursor: pointer; margin-top: 4px;">
</div>
<div class="settings-group" style="flex: 1; min-width: 140px;">
<label style="color: var(--text-secondary); font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Cor do Texto</label>
<input type="color" id="cartazTextColor" value="#2d3748" style="width: 100%; height: 38px; padding: 2px; border: 1px solid var(--border-light); border-radius: 6px; cursor: pointer; margin-top: 4px;">
</div>
</div>
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.8rem; font-weight: 600; text-transform: uppercase;">Paletas Rápidas</label>
<div style="display: flex; gap: 8px; margin-top: 6px; flex-wrap: wrap;">
<button type="button" class="btn-palette-preset" data-bg="#fff9eb" data-text="#2d3748" style="background: #fff9eb; color: #2d3748; padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.7rem; cursor: pointer; font-weight: bold;">Tons Pastéis</button>
<button type="button" class="btn-palette-preset" data-bg="#e6fffa" data-text="#004d40" style="background: #e6fffa; color: #004d40; padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.7rem; cursor: pointer; font-weight: bold;">Jardim Fresco</button>
<button type="button" class="btn-palette-preset" data-bg="#ebf8ff" data-text="#2b6cb0" style="background: #ebf8ff; color: #2b6cb0; padding: 6px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.7rem; cursor: pointer; font-weight: bold;">Céu Azul</button>
<button type="button" class="btn-palette-preset" data-bg="#1a202c" data-text="#f7fafc" style="background: #1a202c; color: #f7fafc; padding: 6px; border: 1px solid #444; border-radius: 4px; font-size: 0.7rem; cursor: pointer; font-weight: bold;">Lousa Escura</button>
</div>
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<div class="settings-group" style="flex: 1; min-width: 140px;">
<label style="color: var(--text-secondary); font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Fonte do Cartaz</label>
<select id="cartazFontSelect" class="obs-select" style="width: 100%; margin-top: 4px;">
<option value="'Fredoka', sans-serif" selected>🎈 Fredoka (Fofa/Infantil)</option>
<option value="'Caveat', cursive">✍️ Caveat (Manuscrita)</option>
<option value="'Playfair Display', serif">📖 Playfair (Clássica)</option>
<option value="'Outfit', sans-serif">⚡ Outfit (Moderna)</option>
</select>
</div>
<div class="settings-group" style="flex: 1; min-width: 140px;">
<label style="color: var(--text-secondary); font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Estilo de Borda</label>
<select id="cartazBorderSelect" class="obs-select" style="width: 100%; margin-top: 4px;">
<option value="none">Nenhuma</option>
<option value="8px dashed">Pontilhada Colorida</option>
<option value="8px double">Linha Dupla Clássica</option>
<option value="12px solid">Borda Larga Colorida</option>
</select>
</div>
</div>
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.8rem; font-weight: 600; text-transform: uppercase;">Ilustração por IA (Opcional)</label>
<textarea id="cartazPromptImagem" placeholder="Ex: Desenho fofo de brinquedos sorridentes na caixa de brinquedos, aquarela, cores suaves..." class="obs-input" style="width: 100%; min-height: 60px; padding: 8px; margin-top: 4px; font-size: 0.85rem; resize: vertical;"></textarea>
</div>
<button type="button" id="btnGenerateCartaz" style="width: 100%; padding: 12px; background: #10b981; color: white; border: none; border-radius: 10px; font-weight: 700; font-size: 1rem; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); transition: all 0.2s; margin-top: 8px;">
<span>🪄 Criar Cartaz Pedagógico</span>
</button>
<div id="cartazGenerateLoader" style="display: none; align-items: center; justify-content: center; gap: 10px; color: var(--text-secondary); font-size: 0.9rem; padding: 10px; background: rgba(255, 255, 255, 0.02); border-radius: 8px;">
<div class="record-spinner" style="width: 20px; height: 20px; border-width: 2px;"></div>
<span id="cartazLoaderText">Montando e diagramando cartaz...</span>
</div>
</div>
<!-- Lado Direito: Preview & Editor de Código -->
<div style="flex: 1.3; min-width: 360px; display: flex; flex-direction: column; gap: 14px;">
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-light); padding-bottom: 8px;">
<h4 style="margin: 0; color: var(--text-primary);">👁️ Visualização do Cartaz</h4>
<div style="display: flex; gap: 8px;">
<button id="btnEditCartazToggle" class="btn-save-settings" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 5px 10px; font-size: 0.75rem;">✏️ Editar Conteúdo</button>
<button id="btnPrintCartaz" class="btn-save-settings" style="background: #3b82f6; border: none; color: white; padding: 5px 12px; font-size: 0.75rem; font-weight: bold; border-radius: 6px;">🖨️ Imprimir Cartaz</button>
</div>
</div>
<!-- Área de Edição do Conteúdo (Inicialmente Oculta) -->
<div id="cartazEditArea" style="display: none; flex-direction: column; gap: 8px;">
<label style="color: var(--text-secondary); font-size: 0.78rem; font-weight: 600; text-transform: uppercase;">Editor HTML do Cartaz</label>
<textarea id="cartazHtmlEditor" class="obs-input" style="width: 100%; min-height: 120px; font-family: monospace; font-size: 0.85rem; padding: 10px;"></textarea>
<button type="button" id="btnUpdateCartazPreview" style="padding: 6px 12px; align-self: flex-end; background: var(--border-light); border: 1px solid var(--border-light); border-radius: 6px; cursor: pointer; color: var(--text-primary); font-size: 0.8rem;">Atualizar Preview</button>
</div>
<!-- Caixa de Preview Imprimível -->
<div style="background: #2d3748; padding: 12px; border-radius: 12px; border: 1px solid var(--border-light); overflow: auto; display: flex; justify-content: center; align-items: center; flex: 1; min-height: 400px;">
<div id="cartazPrintableArea" style="background: #fff9eb; color: #2d3748; padding: 30px; border-radius: 12px; width: 100%; max-width: 500px; box-sizing: border-box; box-shadow: 0 10px 25px rgba(0,0,0,0.3); transition: all 0.3s; min-height: 600px; display: flex; flex-direction: column; font-family: 'Fredoka', sans-serif;">
<h1 id="cartazPreviewTitle" style="text-align: center; margin-top: 0; font-size: 2.2rem; border-bottom: 2px solid; padding-bottom: 12px; margin-bottom: 20px;">Título do Cartaz</h1>
<!-- Área da Imagem IA no Cartaz -->
<div id="cartazPreviewImageContainer" style="display: none; width: 100%; margin-bottom: 20px; border-radius: 8px; overflow: hidden; max-height: 250px; border: 2px solid rgba(0,0,0,0.1);">
<img id="cartazPreviewImage" src="" style="width: 100%; height: 100%; object-fit: cover;">
</div>
<!-- Conteúdo de Texto do Cartaz -->
<div id="cartazPreviewContent" style="font-size: 1.1rem; line-height: 1.6; flex: 1;">
<p style="text-align: center; color: #718096; font-style: italic;">Clique em "Criar Cartaz Pedagógico" para que a assistente monte e estruture o cartaz ideal para você.</p>
</div>
</div>
</div>
</div>
</div>
<!-- Conteúdo 2: Histórico -->
<div id="panelCartazesHistorico" style="display: none; flex-direction: column; gap: 16px;">
<div id="cartazesHistoryContainer" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px;">
<!-- Preenchido via JS -->
</div>
<div id="cartazesNoData" style="text-align: center; color: var(--text-muted); font-style: italic; padding: 40px 0;">
Nenhum cartaz gerado ainda. Crie um acima!
</div>
</div>
</div>
</div>
</div>
<!-- Bibliotecas externas para renderizar markdown e blocos de código -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
+215
View File
@@ -1335,6 +1335,207 @@ app.delete('/api/poesias/:id', requireAuth, async (req, res) => {
}
});
// ============================================================
// ROTAS DO CRIAR CARTAZES
// ============================================================
app.post('/api/cartazes/generate', requireAuth, async (req, res) => {
const { titulo, tema, corFundo, corTexto, layout, promptImagem } = req.body;
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
if (!titulo || !tema || !layout) {
return res.status(400).json({ error: 'Título, tema e layout são obrigatórios.' });
}
try {
const agentConfig = readAgentConfig();
const agentName = agentConfig.agentName || "Kemily";
// 1. Gerar Conteúdo do Cartaz
const posterPrompt = `Você é a ${agentName}, assistente pedagógica da professora Camila Martella Gasparini Reifonas.
A professora Camila quer criar um Cartaz para sala de aula da Educação Infantil.
Detalhes do Cartaz:
- Título: "${titulo}"
- Tema / Foco Pedagógico: "${tema}"
- Layout Selecionado: "${layout}" (Opções: regras, mural, calendario, livre)
Por favor, gere o CONTEÚDO textual estruturado do cartaz.
REGRAS:
- Deve ser acolhedor, lúdico e muito bem estruturado.
- Use emojis apropriados para crianças.
- Se for layout "regras", crie uma lista de combinados da sala (max 5) de forma positiva (ex: "Cuidar dos nossos brinquedos" ao invés de "Não quebrar os brinquedos").
- Se for "calendario", estruture áreas para o dia da semana, ajudante do dia e tempo (ensolarado, chuvoso).
- Se for "mural", crie seções de recados e novidades da semana.
- Se for "livre", crie um texto poético ou explicativo baseado no tema.
- Mantenha os textos curtos, visuais e fáceis de ler para crianças ou pais.
- Retorne apenas o conteúdo em formato HTML simples (use tags como <h1>, <p>, <ul>, <li>, <strong>, <span> e estilos inline simples se necessário) para renderizarmos no preview do cartaz. Não adicione tags <html> ou <body>, apenas o bloco de conteúdo interno.`;
let conteudoHtml = '';
// Tentar MiniMax M3
try {
console.log('[Cartazes] Gerando conteúdo com MiniMax...');
const result = await callMinimax({
messages: [{ role: 'user', content: posterPrompt }],
temperature: 0.7,
max_tokens: 1500
});
conteudoHtml = result.text;
} catch (err) {
console.error('[Cartazes] Erro MiniMax:', err.message);
}
// Fallback OpenRouter
if (!conteudoHtml && process.env.OPENROUTER_API_KEY) {
try {
console.log('[Cartazes] Fallback OpenRouter...');
const r = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: process.env.OPENROUTER_MODEL || 'openai/gpt-4.1-nano',
messages: [{ role: 'user', content: posterPrompt }],
temperature: 0.7,
max_tokens: 1500
})
});
if (r.ok) {
const data = await r.json();
conteudoHtml = data.choices?.[0]?.message?.content || '';
}
} catch (err) {
console.error('[Cartazes] Erro OpenRouter:', err.message);
}
}
if (!conteudoHtml) {
conteudoHtml = `<h2>${titulo}</h2><p>Tema: ${tema}</p><p>Não foi possível gerar com IA. Edite o conteúdo livremente.</p>`;
}
// Clean HTML from backticks
conteudoHtml = conteudoHtml.replace(/```html|```/g, '').trim();
// 2. Gerar Imagem Ilustrativa se solicitado
let generatedImageUrl = null;
if (promptImagem && promptImagem.trim().length > 3) {
try {
console.log('[Cartazes] Gerando ilustração para o cartaz...');
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
const genResp = await fetch(`${minmBase}/v1/image_generation`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'image-01',
prompt: `${promptImagem}. Estilo ilustração de livro infantil de alta qualidade, 3D Pixar, cores suaves, traço limpo, sem texto.`,
n: 1
})
});
if (genResp.ok) {
const genData = await genResp.json();
const rawImgUrl = genData.data?.image_urls?.[0];
if (rawImgUrl) {
const mediaDir = path.join(__dirname, 'public', 'generated-media');
if (!fs.existsSync(mediaDir)) fs.mkdirSync(mediaDir, { recursive: true });
const imgFetch = await fetch(rawImgUrl);
if (imgFetch.ok) {
const imgBuffer = Buffer.from(await imgFetch.arrayBuffer());
const fileName = `cartaz_${Date.now()}_${Math.floor(Math.random() * 1000)}.jpg`;
fs.writeFileSync(path.join(mediaDir, fileName), imgBuffer);
generatedImageUrl = `/generated-media/${fileName}`;
}
}
}
} catch (imgErr) {
console.error('[Cartazes] Erro na geração de imagem:', imgErr);
}
}
// 3. Salvar no Banco
const dbRes = await dbPool.query(
`INSERT INTO escola.cartazes (usuario_id, titulo, tema, cor_fundo, cor_texto, layout, conteudo, imagem_url)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at`,
[usuarioId, titulo, tema, corFundo || '#ffffff', corTexto || '#1e293b', layout, conteudoHtml, generatedImageUrl]
);
res.json({
success: true,
id: dbRes.rows[0].id,
titulo,
tema,
corFundo,
corTexto,
layout,
conteudo: conteudoHtml,
imagemUrl: generatedImageUrl,
createdAt: dbRes.rows[0].created_at
});
} catch (err) {
console.error('Erro ao criar cartaz:', err);
res.status(500).json({ error: 'Erro ao criar cartaz: ' + err.message });
}
});
app.get('/api/cartazes/list', requireAuth, async (req, res) => {
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
try {
const dbRes = await dbPool.query(
`SELECT id, titulo, tema, cor_fundo as "corFundo", cor_texto as "corTexto", layout, imagem_url as "imagemUrl", created_at as "createdAt"
FROM escola.cartazes WHERE usuario_id = $1 ORDER BY created_at DESC`,
[usuarioId]
);
res.json(dbRes.rows);
} catch (err) {
console.error('Erro ao listar cartazes:', err);
res.status(500).json({ error: 'Erro ao listar cartazes.' });
}
});
app.get('/api/cartazes/:id', requireAuth, async (req, res) => {
const { id } = req.params;
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
try {
const dbRes = await dbPool.query(
`SELECT id, titulo, tema, cor_fundo as "corFundo", cor_texto as "corTexto", layout, conteudo, imagem_url as "imagemUrl", created_at as "createdAt"
FROM escola.cartazes WHERE id = $1 AND usuario_id = $2`,
[id, usuarioId]
);
if (dbRes.rows.length === 0) {
return res.status(404).json({ error: 'Cartaz não encontrado.' });
}
res.json(dbRes.rows[0]);
} catch (err) {
console.error('Erro ao buscar cartaz:', err);
res.status(500).json({ error: 'Erro ao buscar cartaz.' });
}
});
app.delete('/api/cartazes/:id', requireAuth, async (req, res) => {
const { id } = req.params;
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
try {
const dbRes = await dbPool.query(
`DELETE FROM escola.cartazes WHERE id = $1 AND usuario_id = $2`,
[id, usuarioId]
);
if (dbRes.rowCount === 0) {
return res.status(404).json({ error: 'Cartaz não encontrado ou não pertence ao usuário.' });
}
res.json({ success: true, message: 'Cartaz excluído com sucesso.' });
} catch (err) {
console.error('Erro ao deletar cartaz:', err);
res.status(500).json({ error: 'Erro ao deletar cartaz.' });
}
});
// ============================================================
// ROTAS DO MUSICANDO IDEIAS
// ============================================================
@@ -1848,6 +2049,20 @@ async function initDatabase() {
historia TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Cartazes (Criar Cartazes)
CREATE TABLE IF NOT EXISTS escola.cartazes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
usuario_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000',
titulo VARCHAR(255) NOT NULL,
tema VARCHAR(255) NOT NULL,
cor_fundo VARCHAR(50) NOT NULL,
cor_texto VARCHAR(50) NOT NULL,
layout VARCHAR(50) NOT NULL, -- 'mural', 'regras', 'calendario', 'livre'
conteudo TEXT NOT NULL,
imagem_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
`);
// Garantir que a coluna character_description_english exista na tabela projetos_comics