🚀 Auto-deploy: Implementação das ferramentas Garatujas e BrincarAtivo

This commit is contained in:
2026-06-04 23:44:53 +00:00
parent 6866d565ec
commit dcabe19edd
3 changed files with 984 additions and 0 deletions
+557
View File
@@ -4713,9 +4713,566 @@ const initApp = () => {
</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 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 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);
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="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;">
</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>
`;
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);
}
}
// ============================================================
// 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; gap: 6px;">
<span>🏃‍♂️</span> ${escapeHtml(item.faixaEtaria.split(' ')[0])} (${item.faixaEtaria.includes('bem pequenas') ? 'Bem Pequenas' : (item.faixaEtaria.includes('pequenas') ? 'Pequenas' : 'Bebês')})
</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>
`;
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
+159
View File
@@ -240,6 +240,12 @@
<button type="button" class="btn-prompt-mode mode-video" id="barBtnVideoMind" style="border-color: rgba(6, 182, 212, 0.4); color: #22d3ee; background: rgba(6, 182, 212, 0.05);" title="Análise pedagógica e resumo de vídeos do YouTube">
<span class="mode-icon">🎬</span> VideoMind
</button>
<button type="button" class="btn-prompt-mode mode-image" id="barBtnGaratujas" style="border-color: rgba(249, 115, 22, 0.4); color: #fdba74; background: rgba(249, 115, 22, 0.05);" title="Análise formativa de desenhos e rabiscos infantis">
<span class="mode-icon">🎨</span> Garatujas
</button>
<button type="button" class="btn-prompt-mode mode-text" id="barBtnBrincarAtivo" style="border-color: rgba(16, 163, 127, 0.4); color: #a7f3d0; background: rgba(16, 163, 127, 0.05);" title="Criar circuitos psicomotores e brincadeiras ativas">
<span class="mode-icon">🏃‍♂️</span> BrincarAtivo
</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
</button>
@@ -1007,6 +1013,159 @@
</div>
</div>
<!-- Modal: Garatujas (Análise de Desenhos) -->
<div id="garatujasModal" class="settings-modal" style="display: none;">
<div class="settings-modal-content" style="max-width: 750px; 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;">🎨 Garatujas - Análise de Traçado Infantil</h3>
<button id="btnCloseGaratujasModal" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem;">&times;</button>
</div>
<!-- Abas do Modal -->
<div class="settings-tabs" style="padding: 12px 20px 0;">
<button type="button" class="settings-tab active" id="tabGaratujasNova">Nova Análise</button>
<button type="button" class="settings-tab" id="tabGaratujasHistorico">Histórico</button>
</div>
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 18px; flex: 1;">
<!-- CONTEÚDO: NOVA ANÁLISE -->
<div id="contentGaratujasNova" class="tab-content-panel">
<div style="display: flex; flex-direction: column; gap: 16px;">
<!-- NOME DO ALUNO -->
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Nome do Aluno</label>
<input type="text" id="garatujasAlunoInput" placeholder="Ex: Joãozinho Silva" class="obs-input" style="width: 100%; margin-top: 4px; padding: 10px;">
</div>
<!-- FOTO DO DESENHO -->
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Foto do Desenho / Rabisco</label>
<div style="display: flex; gap: 12px; align-items: center; margin-top: 6px;">
<button type="button" id="btnUploadGaratuja" class="btn-change-avatar" style="background: rgba(255,255,255,0.02); display: flex; align-items: center; gap: 8px;">
📁 Escolher Imagem
</button>
<input type="file" id="garatujaFileInput" accept="image/*" style="display: none;">
<span id="garatujaFileStatus" style="font-size: 0.85rem; color: var(--text-muted); font-style: italic;">Nenhum arquivo selecionado</span>
</div>
<div id="garatujaPreviewContainer" style="display: none; margin-top: 10px; border: 1px solid var(--border-light); border-radius: 8px; overflow: hidden; max-width: 250px; aspect-ratio: 4/3; background: rgba(0,0,0,0.1);">
<img id="garatujaImgPreview" src="" style="width: 100%; height: 100%; object-fit: contain;">
</div>
</div>
<!-- BOTÃO GERAR -->
<div style="display: flex; flex-direction: column; gap: 10px; margin-top: 8px;">
<button id="btnGenerateGaratuja" style="background: linear-gradient(135deg, #f97316 0%, #ea580c 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(249, 115, 22, 0.25);">
<span>🎨 Enviar & Analisar Desenho</span>
</button>
<div id="garatujaLoader" 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; border-top-color: #f97316;"></div>
<span id="garatujaLoaderText">Processando imagem...</span>
</div>
</div>
<!-- ÁREA DE RESULTADO (INICIALMENTE OCULTA) -->
<div id="garatujaResultArea" style="display: none; border-top: 1px solid var(--border-light); padding-top: 16px; flex-direction: column; gap: 18px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h4 style="margin: 0; color: var(--text-primary); font-size: 1rem; font-weight: 600;">Laudo de Desenvolvimento</h4>
<div style="display: flex; gap: 8px;">
<button id="btnCopyGaratujaReport" 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;">📋 Copiar</button>
<button id="btnPdfGaratujaReport" 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;">📄 PDF</button>
</div>
</div>
<div id="garatujaReportArea" class="report-content-body" style="background: var(--bg-secondary); padding: 16px; border-radius: 10px; max-height: 350px; overflow-y: auto; border: 1px solid var(--border-light); color: var(--text-primary); font-size: 0.95rem; line-height: 1.6;"></div>
</div>
</div>
</div>
<!-- CONTEÚDO: HISTÓRICO -->
<div id="contentGaratujasHistorico" class="tab-content-panel" style="display: none;">
<div id="garatujasListContainer" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; width: 100%;">
<!-- Itens de lista injetados via JS -->
</div>
<div id="garatujasNoData" style="text-align: center; color: var(--text-muted); font-style: italic; padding: 40px 0; width: 100%;">
Nenhum desenho analisado ainda.
</div>
</div>
</div>
</div>
</div>
<!-- Modal: BrincarAtivo (Circuitos e Brincadeiras) -->
<div id="brincarAtivoModal" class="settings-modal" style="display: none;">
<div class="settings-modal-content" style="max-width: 750px; 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;">🏃‍♂️ BrincarAtivo - Fábrica de Brincadeiras & Circuitos</h3>
<button id="btnCloseBrincarAtivoModal" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem;">&times;</button>
</div>
<!-- Abas do Modal -->
<div class="settings-tabs" style="padding: 12px 20px 0;">
<button type="button" class="settings-tab active" id="tabBrincarAtivoNova">Nova Atividade</button>
<button type="button" class="settings-tab" id="tabBrincarAtivoHistorico">Histórico</button>
</div>
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 18px; flex: 1;">
<!-- CONTEÚDO: NOVA ATIVIDADE -->
<div id="contentBrincarAtivoNova" class="tab-content-panel">
<div style="display: flex; flex-direction: column; gap: 16px;">
<!-- FAIXA ETÁRIA -->
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Faixa Etária (BNCC)</label>
<select id="brincarAtivoFaixaSelect" class="obs-input" style="width: 100%; margin-top: 4px; padding: 10px; background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-light); border-radius: 8px;">
<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)">👧 Crianças bem pequenas (1 ano e 7 meses a 3 anos)</option>
<option value="Crianças pequenas (4 anos a 5 anos e 11 meses)">👦 Crianças pequenas (4 anos a 5 anos e 11 meses)</option>
</select>
</div>
<!-- RECURSOS DISPONÍVEIS -->
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Recursos / Materiais Disponíveis</label>
<textarea id="brincarAtivoRecursosInput" placeholder="Ex: Caixas de papelão, garrafas plásticas vazias, giz para riscar o chão, cones e bambolês." class="obs-input" rows="3" style="width: 100%; margin-top: 4px; padding: 10px; font-family: inherit; resize: vertical;"></textarea>
</div>
<!-- BOTÃO GERAR -->
<div style="display: flex; flex-direction: column; gap: 10px; margin-top: 8px;">
<button id="btnGenerateBrincarAtivo" style="background: linear-gradient(135deg, #10a37f 0%, #0a5f49 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, 163, 127, 0.25);">
<span>🏃‍♂️ Criar Circuito / Brincadeira</span>
</button>
<div id="brincarAtivoLoader" 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="brincarAtivoLoaderText">Montando planejamento psicomotor...</span>
</div>
</div>
<!-- ÁREA DE RESULTADO (INICIALMENTE OCULTA) -->
<div id="brincarAtivoResultArea" style="display: none; border-top: 1px solid var(--border-light); padding-top: 16px; flex-direction: column; gap: 18px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h4 style="margin: 0; color: var(--text-primary); font-size: 1rem; font-weight: 600;">Planejamento Pedagógico</h4>
<div style="display: flex; gap: 8px;">
<button id="btnCopyBrincarAtivoReport" 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;">📋 Copiar</button>
<button id="btnPdfBrincarAtivoReport" 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;">📄 PDF</button>
</div>
</div>
<div id="brincarAtivoReportArea" class="report-content-body" style="background: var(--bg-secondary); padding: 16px; border-radius: 10px; max-height: 350px; overflow-y: auto; border: 1px solid var(--border-light); color: var(--text-primary); font-size: 0.95rem; line-height: 1.6;"></div>
</div>
</div>
</div>
<!-- CONTEÚDO: HISTÓRICO -->
<div id="contentBrincarAtivoHistorico" class="tab-content-panel" style="display: none;">
<div id="brincarAtivoListContainer" style="display: flex; flex-direction: column; gap: 12px; width: 100%;">
<!-- Itens de lista injetados via JS -->
</div>
<div id="brincarAtivoNoData" style="text-align: center; color: var(--text-muted); font-style: italic; padding: 40px 0; width: 100%;">
Nenhuma atividade gerada ainda.
</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>
+268
View File
@@ -789,6 +789,254 @@ Retorne a resposta estruturada estritamente em Markdown. Separe as seções usan
}
});
// ============================================================
// ROTAS DO GARATUJAS (ANALISADOR DE TRAÇADO INFANTIL)
// ============================================================
app.post('/api/garatujas/analyze', requireAuth, multerUpload.single('image'), async (req, res) => {
const { alunoName } = req.body;
const file = req.file;
if (!alunoName) {
return res.status(400).json({ error: 'O nome do aluno é obrigatório.' });
}
if (!file) {
return res.status(400).json({ error: 'A foto do desenho é obrigatória.' });
}
try {
const fs = require('fs');
const mediaDir = path.join(__dirname, 'public', 'generated-media');
if (!fs.existsSync(mediaDir)) {
fs.mkdirSync(mediaDir, { recursive: true });
}
const ext = path.extname(file.originalname) || '.png';
const fileName = `garatuja_${Date.now()}${ext}`;
const filePath = path.join(mediaDir, fileName);
fs.writeFileSync(filePath, file.buffer);
const imageUrl = `/generated-media/${fileName}`;
const base64Image = file.buffer.toString('base64');
const mimeType = file.mimetype || 'image/png';
const response = 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: 'google/gemini-2.5-flash',
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: `Você é um psicopedagogo e especialista em desenvolvimento infantil no Brasil.
Analise a imagem deste desenho/rabisco feito pelo aluno(a) "${alunoName}" (até 6 anos de idade).
Identifique e descreva com base na literatura científica (como Piaget, Luquet, Lowenfeld):
1. O estágio do desenho infantil em que a criança se encontra (ex: Garatuja Desordenada, Garatuja Ordenada, Pré-Esquematismo, etc.) justificando com elementos visuais identificados no desenho.
2. A análise do desenvolvimento cognitivo e da coordenação motora fina expressa nos traços.
3. Propostas práticas de estimulação pedagógica personalizadas para o professor aplicar com o aluno "${alunoName}" em sala de aula para avançar para o próximo estágio.
Retorne a resposta estruturada em Markdown, usando títulos claros:
# Estágio do Desenho e Análise Psicomotora
[Sua análise aqui]
# Diagnóstico de Desenvolvimento Cognitivo
[Seu diagnóstico aqui]
# Sugestões de Atividades para Estimular ${alunoName}
[Suas propostas aqui]`
},
{
type: 'image_url',
image_url: {
url: `data:${mimeType};base64,${base64Image}`
}
}
]
}
]
})
});
if (!response.ok) {
const errText = await response.text();
throw new Error(`Erro na API de visão: ${errText}`);
}
const resData = await response.json();
const analise = resData.choices?.[0]?.message?.content || '';
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
const dbRes = await dbPool.query(
`INSERT INTO escola.garatujas (usuario_id, aluno_name, image_url, analise)
VALUES ($1, $2, $3, $4) RETURNING id, created_at`,
[usuarioId, alunoName, imageUrl, analise]
);
res.json({
success: true,
id: dbRes.rows[0].id,
alunoName,
imageUrl,
analise,
createdAt: dbRes.rows[0].created_at
});
} catch (err) {
console.error('Erro na análise de garatujas:', err);
res.status(500).json({ error: 'Erro ao analisar imagem: ' + err.message });
}
});
app.get('/api/garatujas/list', requireAuth, async (req, res) => {
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
try {
const dbRes = await dbPool.query(
`SELECT id, aluno_name as "alunoName", image_url as "imageUrl", created_at as "createdAt"
FROM escola.garatujas
WHERE usuario_id = $1
ORDER BY created_at DESC`,
[usuarioId]
);
res.json(dbRes.rows);
} catch (err) {
console.error('Erro ao listar garatujas:', err);
res.status(500).json({ error: 'Erro ao obter histórico de desenhos.' });
}
});
app.get('/api/garatujas/: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, aluno_name as "alunoName", image_url as "imageUrl", analise, created_at as "createdAt"
FROM escola.garatujas
WHERE id = $1 AND usuario_id = $2`,
[id, usuarioId]
);
if (dbRes.rows.length === 0) {
return res.status(404).json({ error: 'Análise não encontrada.' });
}
res.json(dbRes.rows[0]);
} catch (err) {
console.error('Erro ao buscar garatuja:', err);
res.status(500).json({ error: 'Erro ao buscar análise.' });
}
});
// ============================================================
// ROTAS DO BRINCARATIVO (CIRCUITOS PSICOMOTORES E BRINCADEIRAS)
// ============================================================
app.post('/api/brincar-ativo/generate', requireAuth, async (req, res) => {
const { faixaEtaria, recursos } = req.body;
if (!faixaEtaria) {
return res.status(400).json({ error: 'A faixa etária é obrigatória.' });
}
if (!recursos) {
return res.status(400).json({ error: 'A descrição dos recursos é obrigatória.' });
}
try {
const prompt = `Você é um educador físico e pedagogo especializado na Educação Infantil (crianças de 0 a 6 anos) de acordo com a BNCC no Brasil.
Gere uma proposta de circuito psicomotor ou brincadeira ativa adequada para a seguinte faixa etária e com os recursos disponíveis:
Faixa Etária: ${faixaEtaria}
Recursos e Materiais Disponíveis: ${recursos}
Instruções para o conteúdo:
1. Dê um título lúdico e atraente para a atividade.
2. Especifique os Objetivos de Aprendizagem e Desenvolvimento da BNCC (incluindo códigos, como EI01CG02, EI02CG05, etc.).
3. Descreva o Passo a Passo detalhado de como montar o circuito/brincadeira no espaço e como guiar as crianças.
4. Diga o que o professor deve observar durante a atividade para avaliar o desenvolvimento psicomotor dos alunos.
Retorne a resposta estritamente em Markdown estruturado:
# Atividade: [Nome Lúdico]
**Faixa Etária:** ${faixaEtaria}
**Campos de Experiência (BNCC):** [Campos e Códigos]
## 📋 Objetivos
[Objetivos]
## 🛠️ Como Montar e Conduzir
[Passo a passo]
## 🔍 O que Observar (Avaliação Formativa)
[Dicas de observação]`;
const aiResponse = await callMinimax({
system: "Você é um especialista em educação física e psicomotricidade na educação infantil brasileira.",
messages: [{ role: 'user', content: prompt }],
temperature: 0.5,
max_tokens: 3000
});
const atividade = aiResponse.text || '';
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
const dbRes = await dbPool.query(
`INSERT INTO escola.brincar_ativo (usuario_id, faixa_etaria, recursos, atividade)
VALUES ($1, $2, $3, $4) RETURNING id, created_at`,
[usuarioId, faixaEtaria, recursos, atividade]
);
res.json({
success: true,
id: dbRes.rows[0].id,
faixaEtaria,
recursos,
atividade,
createdAt: dbRes.rows[0].created_at
});
} catch (err) {
console.error('Erro no BrincarAtivo:', err);
res.status(500).json({ error: 'Erro ao gerar atividade com a IA: ' + err.message });
}
});
app.get('/api/brincar-ativo/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 as "faixaEtaria", recursos, created_at as "createdAt"
FROM escola.brincar_ativo
WHERE usuario_id = $1
ORDER BY created_at DESC`,
[usuarioId]
);
res.json(dbRes.rows);
} catch (err) {
console.error('Erro ao listar atividades:', err);
res.status(500).json({ error: 'Erro ao obter lista de atividades.' });
}
});
app.get('/api/brincar-ativo/: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, faixa_etaria as "faixaEtaria", recursos, atividade, created_at as "createdAt"
FROM escola.brincar_ativo
WHERE id = $1 AND usuario_id = $2`,
[id, usuarioId]
);
if (dbRes.rows.length === 0) {
return res.status(404).json({ error: 'Atividade não encontrada.' });
}
res.json(dbRes.rows[0]);
} catch (err) {
console.error('Erro ao buscar atividade:', err);
res.status(500).json({ error: 'Erro ao buscar atividade.' });
}
});
// Rota GET para obter configurações do agente
app.get('/api/agent-config', requireAuth, (req, res) => {
const config = readAgentConfig();
@@ -968,6 +1216,26 @@ async function initDatabase() {
descricao_fisica_en TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Garatujas (Analisador de Traçado Infantil)
CREATE TABLE IF NOT EXISTS escola.garatujas (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
usuario_id UUID NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000',
aluno_name VARCHAR(255) NOT NULL,
image_url TEXT NOT NULL,
analise TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- BrincarAtivo (Circuitos Psicomotores e Brincadeiras)
CREATE TABLE IF NOT EXISTS escola.brincar_ativo (
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,
recursos TEXT NOT NULL,
atividade TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
`);
// Garantir que a coluna character_description_english exista na tabela projetos_comics