Criando novo módulo Estúdio de Poesia e renomeando botões

This commit is contained in:
2026-06-05 13:50:39 +00:00
parent 0456617717
commit 7fa88481b7
3 changed files with 410 additions and 4 deletions
+191
View File
@@ -5170,6 +5170,197 @@ const initApp = () => {
}
}
// ============================================================
// 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 element = document.createElement('div');
element.innerHTML = window.marked ? marked.parse(currentPoesia) : currentPoesia;
element.style.padding = '20px';
element.style.fontFamily = 'Arial, sans-serif';
element.style.color = '#333';
html2pdf().set({
margin: 15,
filename: 'poesia_pedagogica.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
}).from(element).save();
});
}
// 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;';
div.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<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>
`;
div.addEventListener('click', async () => {
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)
// ============================================================
+86 -4
View File
@@ -239,7 +239,7 @@
<span class="mode-icon">🎨</span> Atividade BNCC
</button>
<button type="button" class="btn-prompt-mode mode-music" data-mode="AUDIO" title="Compor e gerar uma música pedagógica">
<span class="mode-icon">🎵</span> Criar Letra de Música
<span class="mode-icon">🎵</span> Musicando Ideias
</button>
<button type="button" class="btn-prompt-mode mode-video" data-mode="VIDEO" title="Criar um roteiro ou animação de vídeo">
<span class="mode-icon">🎬</span> Criar Vídeo
@@ -247,11 +247,11 @@
<button type="button" class="btn-prompt-mode mode-image" data-mode="IMAGE" title="Gerar ou desenhar uma ilustração educativa">
<span class="mode-icon">🎨</span> Criar Imagem
</button>
<button type="button" class="btn-prompt-mode mode-poetry" data-mode="POETRY" title="Escrever uma poesia infantil pedagógica">
<span class="mode-icon">📜</span> Criar Poesia
<button type="button" class="btn-prompt-mode mode-poetry" id="barBtnPoesia" title="Escrever uma poesia infantil pedagógica">
<span class="mode-icon">📜</span> Estúdio de Poesia
</button>
<button type="button" class="btn-prompt-mode mode-text" data-mode="TEXT" title="Escrever histórias, relatórios ou conversar">
<span class="mode-icon">✍️</span> Criar História / Chat
<span class="mode-icon">✍️</span> Inventando Historias
</button>
</div>
<form id="chatForm">
@@ -736,6 +736,88 @@
</div>
</div>
<!-- Modal: Estúdio de Poesia -->
<div id="poesiaModal" class="settings-modal" style="display: none;">
<div class="settings-modal-content" style="max-width: 600px; width: 95%; max-height: 90vh; 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 Poesia Camila AI</h3>
<div style="display: flex; gap: 10px;">
<button id="btnOpenPoesiaHistory" class="btn-save-settings" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-primary); padding: 6px 12px; font-size: 0.8rem;">
🕒 Histórico
</button>
<button id="btnClosePoesiaModal" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem;">&times;</button>
</div>
</div>
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 18px; flex: 1;">
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">1. Faixa Etária (Idade da Criança)</label>
<select id="poesiaFaixaEtaria" class="obs-select" style="width: 100%; margin-top: 4px;">
<option value="Bebês (0 a 1 ano e 6 meses)">Bebês (0 a 1 ano e 6 meses)</option>
<option value="Crianças bem pequenas (1 ano e 7 meses a 3 anos e 11 meses)">Crianças bem pequenas (1 ano e 7 meses a 3 anos e 11 meses)</option>
<option value="Crianças pequenas (4 anos a 5 anos e 11 meses)" selected>Crianças pequenas (4 anos a 5 anos e 11 meses)</option>
<option value="Ensino Fundamental I (6 a 10 anos)">Ensino Fundamental I (6 a 10 anos)</option>
</select>
</div>
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">2. Quantidade de Estrofes</label>
<div class="music-option-grid" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-top: 4px;">
<button type="button" class="btn-poesia-estrofes" data-estrofes="2">2 Estrofes</button>
<button type="button" class="btn-poesia-estrofes active" data-estrofes="3">3 Estrofes</button>
<button type="button" class="btn-poesia-estrofes" data-estrofes="4">4 Estrofes</button>
<button type="button" class="btn-poesia-estrofes" data-estrofes="5">5 Estrofes</button>
</div>
</div>
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">3. Tema da Poesia</label>
<textarea id="poesiaTemaInput" placeholder="Ex: Higiene pessoal, amizade na escola, os animais da fazenda..." class="obs-input" style="width: 100%; min-height: 80px; padding: 10px; resize: vertical; margin-top: 4px; line-height: 1.4;"></textarea>
</div>
<div style="display: flex; flex-direction: column; gap: 10px; margin-top: 8px;">
<button id="btnGeneratePoesia" style="background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; border: none; padding: 12px; border-radius: 8px; font-weight: 600; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: opacity 0.2s; box-shadow: 0 4px 12px rgba(16, 185, 129, 0.25);">
<span>📜 Criar Poesia Personalizada</span>
</button>
<div id="poesiaGenerateLoader" 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>A IA está buscando inspiração e compondo sua poesia...</span>
</div>
</div>
<div id="poesiaResultArea" style="display: none; border-top: 1px solid var(--border-light); padding-top: 16px; flex-direction: column; gap: 14px;">
<h4 style="margin: 0; color: var(--text-primary); font-size: 1rem; display: flex; align-items: center; gap: 6px;">✨ Sua Poesia Está Pronta!</h4>
<div style="display: flex; flex-direction: column; gap: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<label style="color: var(--text-secondary); font-size: 0.78rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Poesia Gerada</label>
<div style="display: flex; gap: 8px;">
<button id="btnCopyPoesia" style="background: none; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 5px 10px; border-radius: 6px; font-size: 0.75rem; cursor: pointer; transition: all 0.2s;">📋 Copiar</button>
<button id="btnDownloadPoesiaPdf" style="background: none; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 5px 10px; border-radius: 6px; font-size: 0.75rem; cursor: pointer; transition: all 0.2s;">💾 Baixar PDF</button>
</div>
</div>
<div id="poesiaTextOutput" class="markdown-body" style="background: var(--bg-secondary); padding: 16px; border-radius: 10px; font-size: 1rem; line-height: 1.6; border: 1px solid var(--border-light); color: var(--text-primary); margin: 0; min-height: 150px;"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal: Histórico de Poesias -->
<div id="poesiaHistoryModal" class="settings-modal" style="display: none; z-index: 1002;">
<div class="settings-modal-content" style="max-width: 600px; width: 95%; max-height: 80vh; 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;">🕒 Histórico de Poesias</h3>
<button id="btnClosePoesiaHistory" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem;">&times;</button>
</div>
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; flex: 1;">
<div id="poesiaHistoryList" style="display: flex; flex-direction: column; gap: 10px;">
<p style="color: var(--text-secondary); font-size: 0.9rem; text-align: center; margin-top: 20px;">Carregando histórico...</p>
</div>
</div>
</div>
</div>
<!-- Modal: Fábrica de Quadrinhos (Criação de histórias pedagógicas visuais) -->
<div id="fabricaQuadrinhosModal" class="settings-modal" style="display: none;">
<div class="settings-modal-content" style="max-width: 800px; width: 95%; max-height: 90vh; display: flex; flex-direction: column;">
+133
View File
@@ -1145,6 +1145,128 @@ app.delete('/api/brincar-ativo/:id', requireAuth, async (req, res) => {
}
});
// ============================================================
// ROTAS DO ESTÚDIO DE POESIA
// ============================================================
app.post('/api/poesias/generate', requireAuth, async (req, res) => {
const { faixaEtaria, estrofes, tema } = req.body;
if (!faixaEtaria || !estrofes || !tema) {
return res.status(400).json({ error: 'Faixa etária, quantidade de estrofes e tema são obrigatórios.' });
}
try {
let poesiaText = '';
const systemInstruction = `Você é um poeta pedagógico infantil especializado em criar poesias ritmadas, sonoras e adequadas para o desenvolvimento infantil no Brasil.
REGRAS RÍGIDAS:
- Escreva a poesia para a faixa etária: ${faixaEtaria}
- A poesia DEVE ter EXATAMENTE ${estrofes} estrofes.
- O tema da poesia é: "${tema}"
- Use rimas claras e vocabulário apropriado para a idade.
- Sua resposta deve conter APENAS a poesia formatada em Markdown, sem NENHUM comentário adicional, explicação, ou blocos de pensamento.
- Responda EXCLUSIVAMENTE em Português do Brasil.`;
// Utilizando o Gemini diretamente
if (process.env.GOOGLE_AI_API_KEY) {
console.log('[Poesia] Usando API direta do Google Gemini...');
const geminiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${process.env.GOOGLE_AI_MODEL || 'gemini-2.5-flash'}:generateContent?key=${process.env.GOOGLE_AI_API_KEY}`;
const response = await fetch(geminiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: systemInstruction }] }],
generationConfig: { temperature: 0.7, maxOutputTokens: 2000 }
})
});
if (!response.ok) {
const errText = await response.text();
throw new Error(`Erro na API oficial do Gemini: ${errText}`);
}
const resData = await response.json();
poesiaText = resData.candidates?.[0]?.content?.parts?.[0]?.text || '';
// Remover qualquer tag <think> residual (embora a instrução já previna)
poesiaText = poesiaText.replace(/<think>[\s\S]*?(?:<\/think>|$)/g, '').trim();
} else {
throw new Error('Chave GOOGLE_AI_API_KEY não configurada no servidor.');
}
if (!poesiaText) {
throw new Error('Nenhuma poesia gerada.');
}
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
const dbRes = await dbPool.query(
`INSERT INTO escola.poesias (usuario_id, faixa_etaria, estrofes, tema, poesia)
VALUES ($1, $2, $3, $4, $5) RETURNING id, created_at`,
[usuarioId, faixaEtaria, parseInt(estrofes), tema, poesiaText]
);
res.json({
success: true,
id: dbRes.rows[0].id,
poesia: poesiaText,
createdAt: dbRes.rows[0].created_at
});
} catch (err) {
console.error('Erro na geração de poesia:', err);
res.status(500).json({ error: 'Erro ao gerar poesia: ' + err.message });
}
});
app.get('/api/poesias/list', requireAuth, async (req, res) => {
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
try {
const dbRes = await dbPool.query(
`SELECT id, faixa_etaria, estrofes, tema, created_at FROM escola.poesias WHERE usuario_id = $1 ORDER BY created_at DESC`,
[usuarioId]
);
res.json(dbRes.rows);
} catch (err) {
console.error('Erro ao listar poesias:', err);
res.status(500).json({ error: 'Erro ao listar histórico de poesias.' });
}
});
app.get('/api/poesias/: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 * FROM escola.poesias WHERE id = $1 AND usuario_id = $2`,
[id, usuarioId]
);
if (dbRes.rows.length === 0) {
return res.status(404).json({ error: 'Poesia não encontrada.' });
}
res.json(dbRes.rows[0]);
} catch (err) {
console.error('Erro ao buscar poesia:', err);
res.status(500).json({ error: 'Erro ao buscar poesia.' });
}
});
app.delete('/api/poesias/: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.poesias WHERE id = $1 AND usuario_id = $2`,
[id, usuarioId]
);
if (dbRes.rowCount === 0) {
return res.status(404).json({ error: 'Poesia não encontrada ou não pertence ao usuário.' });
}
res.json({ success: true, message: 'Poesia deletada com sucesso.' });
} catch (err) {
console.error('Erro ao deletar poesia:', err);
res.status(500).json({ error: 'Erro ao deletar poesia.' });
}
});
// Rota GET para obter configurações do agente
app.get('/api/agent-config', requireAuth, (req, res) => {
@@ -1345,6 +1467,17 @@ async function initDatabase() {
atividade TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Poesias (Estúdio de Poesia)
CREATE TABLE IF NOT EXISTS escola.poesias (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
usuario_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000',
faixa_etaria VARCHAR(100) NOT NULL,
estrofes INTEGER NOT NULL,
tema TEXT NOT NULL,
poesia TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
`);
// Garantir que a coluna character_description_english exista na tabela projetos_comics