Feature: Inventando Histórias profissional com 3-stage pipeline narrativa, vinculacao a turmas/alunos, ilustracoes por cena, leitor interativo, TTS narrado e exportacao PDF/TXT

This commit is contained in:
2026-08-31 11:25:33 +00:00
parent 3931af93db
commit 837762c1af
4 changed files with 1193 additions and 175 deletions
+528 -66
View File
@@ -7694,8 +7694,67 @@ window.toggleCustomAudio = (btn) => {
// ============================================================ // ============================================================
// INVENTANDO HISTORIAS // INVENTANDO HISTÓRIAS (ESTÚDIO PROFISSIONAL)
// ============================================================ // ============================================================
const HISTORIA_TEMPLATES = {
arca: {
titulo: 'A Arca da Imaginação',
objeto: 'Arca Mágica de Madeira',
tema: 'As crianças encontram uma arca misteriosa no parquinho que se abre apenas quando todos dão as mãos e compartilham uma ideia criativa.',
estilo: 'Lúdico, Afetivo e Dialogado',
faixa: 'Crianças pequenas (4 anos a 5 anos e 11 meses)',
visual: 'Cartoon colorido e acolhedor'
},
horta: {
titulo: 'O Mistério da Horta Encantada',
objeto: 'Semente Brilhante de Girassol',
tema: 'A turma planta uma sementinha brilhante na horta da escola e descobre que ela cresce quando ouve canções de carinho e risadas.',
estilo: 'Contação Clássica com Suspense Leve',
faixa: 'Crianças pequenas (4 anos a 5 anos e 11 meses)',
visual: 'Aquarela suave infantil'
},
dinossauro: {
titulo: 'O Pequeno Dinossauro Amigo',
objeto: 'Pegada Colorida de Brinquedo',
tema: 'Um pequeno dinossauro verde e tímido aparece na hora do lanche procurando amigos para brincar de roda e desenhar.',
estilo: 'Lúdico, Afetivo e Dialogado',
faixa: 'Crianças bem pequenas (1 ano e 7 meses a 3 anos e 11 meses)',
visual: 'Cartoon colorido e acolhedor'
},
chuva: {
titulo: 'O Dia em que a Chuva Dançou',
objeto: 'Galochas Amarelas Saltitantes',
tema: 'Em um dia chuvoso, as gotas batem na janela fazendo música e convidam as crianças para uma dança aconchegante dentro da sala.',
estilo: 'Roda de Conversa Interativa',
faixa: 'Crianças pequenas (4 anos a 5 anos e 11 meses)',
visual: 'Aquarela suave infantil'
},
bolhas: {
titulo: 'O Reino Mágico das Bolhas de Sabão',
objeto: 'Varinha de Fazer Bolhas Gigantes',
tema: 'Cada bolha de sabão que as crianças assopram no pátio leva um desejo de abraço e alegria até as nuvens coloridas.',
estilo: 'Lúdico, Afetivo e Dialogado',
faixa: 'Bebês (0 a 1 ano e 6 meses)',
visual: 'Giz de cera e textura lúdica'
},
brinquedos: {
titulo: 'A Reunião Secreta dos Brinquedos',
objeto: 'Ursinho de Pano Guardião',
tema: 'Na hora do descanso, os brinquedos se organizam com muito capricho para esperar o acordar dos alunos com uma surpresa de blocos montados.',
estilo: 'Aventura e Descoberta na Escola',
faixa: 'Crianças pequenas (4 anos a 5 anos e 11 meses)',
visual: 'Livro infantil clássico e detalhado'
},
emocoes: {
titulo: 'O Monstrinho das Cores e dos Sentimentos',
objeto: 'Pote Brilhante das Emoções',
tema: 'Um monstrinho amigo muda de cor quando sente alegria (amarelo), calma (azul) ou saudade (rosa), ensinando a turma a expressar o que sente.',
estilo: 'Roda de Conversa Interativa',
faixa: 'Crianças pequenas (4 anos a 5 anos e 11 meses)',
visual: 'Cartoon colorido e acolhedor'
}
};
const barBtnHistoria = document.getElementById('barBtnHistoria'); const barBtnHistoria = document.getElementById('barBtnHistoria');
const historiaModal = document.getElementById('historiaModal'); const historiaModal = document.getElementById('historiaModal');
const btnCloseHistoriaModal = document.getElementById('btnCloseHistoriaModal'); const btnCloseHistoriaModal = document.getElementById('btnCloseHistoriaModal');
@@ -7703,35 +7762,118 @@ window.toggleCustomAudio = (btn) => {
const historiaHistoryModal = document.getElementById('historiaHistoryModal'); const historiaHistoryModal = document.getElementById('historiaHistoryModal');
const btnCloseHistoriaHistory = document.getElementById('btnCloseHistoriaHistory'); const btnCloseHistoriaHistory = document.getElementById('btnCloseHistoriaHistory');
const historiaTemplateSelect = document.getElementById('historiaTemplateSelect');
const historiaTurmaSelect = document.getElementById('historiaTurmaSelect');
const historiaPersonagemInput = document.getElementById('historiaPersonagemInput');
const historiaObjetoMagicoInput = document.getElementById('historiaObjetoMagicoInput');
const historiaFaixaEtaria = document.getElementById('historiaFaixaEtaria'); const historiaFaixaEtaria = document.getElementById('historiaFaixaEtaria');
const historiaParagrafos = document.getElementById('historiaParagrafos'); const historiaParagrafos = document.getElementById('historiaParagrafos');
const historiaPersonagens = document.getElementById('historiaPersonagens');
const historiaEstilo = document.getElementById('historiaEstilo'); const historiaEstilo = document.getElementById('historiaEstilo');
const historiaEstiloVisual = document.getElementById('historiaEstiloVisual');
const historiaTemaInput = document.getElementById('historiaTemaInput'); const historiaTemaInput = document.getElementById('historiaTemaInput');
const btnGenerateHistoria = document.getElementById('btnGenerateHistoria'); const btnGenerateHistoria = document.getElementById('btnGenerateHistoria');
const historiaGenerateLoader = document.getElementById('historiaGenerateLoader'); const historiaGenerateLoader = document.getElementById('historiaGenerateLoader');
const historiaResultArea = document.getElementById('historiaResultArea');
const historiaTextOutput = document.getElementById('historiaTextOutput');
const btnCopyHistoria = document.getElementById('btnCopyHistoria');
const btnDownloadHistoriaPdf = document.getElementById('btnDownloadHistoriaPdf');
let currentHistoria = ''; // Elementos do Leitor e Visualizador
const historiaPlaceholderText = document.getElementById('historiaPlaceholderText');
const historiaActiveBox = document.getElementById('historiaActiveBox');
const historiaActiveTitle = document.getElementById('historiaActiveTitle');
const historiaActiveSubtitle = document.getElementById('historiaActiveSubtitle');
const historiaBnccBadge = document.getElementById('historiaBnccBadge');
if (barBtnHistoria) { // Player de Áudio
barBtnHistoria.addEventListener('click', () => { const historiaAudioPlayerBox = document.getElementById('historiaAudioPlayerBox');
historiaModal.style.display = 'flex'; const historiaAudioPlayer = document.getElementById('historiaAudioPlayer');
historiaTemaInput.value = ''; const btnDownloadHistoriaAudio = document.getElementById('btnDownloadHistoriaAudio');
historiaResultArea.style.display = 'none'; const historiaWaveBars = document.querySelectorAll('.wave-bar-historia');
historiaGenerateLoader.style.display = 'none';
btnGenerateHistoria.disabled = false; // Abas
currentHistoria = ''; const historiaContentTabs = document.getElementById('historiaContentTabs');
const btnTabHistoriaCenas = document.getElementById('btnTabHistoriaCenas');
const btnTabHistoriaTexto = document.getElementById('btnTabHistoriaTexto');
const btnTabHistoriaAtividades = document.getElementById('btnTabHistoriaAtividades');
const historiaCenasView = document.getElementById('historiaCenasView');
const historiaTextoView = document.getElementById('historiaTextoView');
const historiaAtividadesView = document.getElementById('historiaAtividadesView');
// Cenas Interativas
const btnPrevCena = document.getElementById('btnPrevCena');
const btnNextCena = document.getElementById('btnNextCena');
const historiaCenaIndicator = document.getElementById('historiaCenaIndicator');
const historiaCenaTitle = document.getElementById('historiaCenaTitle');
const historiaCenaText = document.getElementById('historiaCenaText');
const historiaCenaImageContainer = document.getElementById('historiaCenaImageContainer');
const historiaCenaImg = document.getElementById('historiaCenaImg');
const btnGenerateSceneImage = document.getElementById('btnGenerateSceneImage');
// BNCC e Perguntas
const historiaBnccDesc = document.getElementById('historiaBnccDesc');
const historiaPerguntasList = document.getElementById('historiaPerguntasList');
// Ações
const btnCopyActiveHistoria = document.getElementById('btnCopyActiveHistoria');
const btnDownloadActiveHistoriaTxt = document.getElementById('btnDownloadActiveHistoriaTxt');
const btnSendHistoriaToMusic = document.getElementById('btnSendHistoriaToMusic');
const btnExportActiveHistoriaPdf = document.getElementById('btnExportActiveHistoriaPdf');
let currentHistoriaData = null;
let currentSceneIndex = 0;
// Carregar turmas no select de histórias
async function loadTurmasForHistoria() {
if (!historiaTurmaSelect) return;
try {
const res = await fetch('/api/turmas');
if (res.ok) {
const turmas = await res.json();
historiaTurmaSelect.innerHTML = '<option value="">-- Selecionar Turma --</option>';
turmas.forEach(t => {
const opt = document.createElement('option');
opt.value = t.id;
opt.textContent = `${t.nome} (${t.ano_letivo || '2026'})`;
historiaTurmaSelect.appendChild(opt);
});
}
} catch (e) {
console.warn('Erro ao carregar turmas para histórias:', e);
}
}
// Mudança de template
if (historiaTemplateSelect) {
historiaTemplateSelect.addEventListener('change', () => {
const key = historiaTemplateSelect.value;
if (key && HISTORIA_TEMPLATES[key]) {
const tpl = HISTORIA_TEMPLATES[key];
historiaTemaInput.value = tpl.tema;
if (historiaObjetoMagicoInput) historiaObjetoMagicoInput.value = tpl.objeto;
if (historiaEstilo) historiaEstilo.value = tpl.estilo;
if (historiaFaixaEtaria) historiaFaixaEtaria.value = tpl.faixa;
if (historiaEstiloVisual) historiaEstiloVisual.value = tpl.visual;
}
}); });
} }
if (btnCloseHistoriaModal) historiaModal.addEventListener('click', (e) => { if(e.target === historiaModal) historiaModal.style.display = 'none'; }); // Abertura do Modal
if (btnCloseHistoriaModal) btnCloseHistoriaModal.addEventListener('click', () => historiaModal.style.display = 'none'); if (barBtnHistoria) {
barBtnHistoria.addEventListener('click', () => {
historiaModal.style.display = 'flex';
loadTurmasForHistoria();
if (!currentHistoriaData) {
if (historiaPlaceholderText) historiaPlaceholderText.style.display = 'block';
if (historiaActiveBox) historiaActiveBox.style.display = 'none';
if (historiaAudioPlayerBox) historiaAudioPlayerBox.style.display = 'none';
if (historiaContentTabs) historiaContentTabs.style.display = 'none';
}
});
}
if (btnCloseHistoriaModal) {
historiaModal.addEventListener('click', (e) => { if (e.target === historiaModal) historiaModal.style.display = 'none'; });
btnCloseHistoriaModal.addEventListener('click', () => historiaModal.style.display = 'none');
}
// Histórico
if (btnOpenHistoriaHistory) { if (btnOpenHistoriaHistory) {
btnOpenHistoriaHistory.addEventListener('click', () => { btnOpenHistoriaHistory.addEventListener('click', () => {
historiaHistoryModal.style.display = 'flex'; historiaHistoryModal.style.display = 'flex';
@@ -7739,93 +7881,358 @@ window.toggleCustomAudio = (btn) => {
}); });
} }
if (btnCloseHistoriaHistory) btnCloseHistoriaHistory.addEventListener('click', () => historiaHistoryModal.style.display = 'none'); if (btnCloseHistoriaHistory) btnCloseHistoriaHistory.addEventListener('click', () => historiaHistoryModal.style.display = 'none');
historiaHistoryModal.addEventListener('click', (e) => { if(e.target === historiaHistoryModal) historiaHistoryModal.style.display = 'none'; }); historiaHistoryModal.addEventListener('click', (e) => { if (e.target === historiaHistoryModal) historiaHistoryModal.style.display = 'none'; });
// Alternância de Abas
function switchHistoriaTab(tab) {
[btnTabHistoriaCenas, btnTabHistoriaTexto, btnTabHistoriaAtividades].forEach(b => {
if (b) {
b.classList.remove('active');
b.style.background = 'transparent';
b.style.color = 'var(--text-secondary)';
}
});
if (historiaCenasView) historiaCenasView.style.display = 'none';
if (historiaTextoView) historiaTextoView.style.display = 'none';
if (historiaAtividadesView) historiaAtividadesView.style.display = 'none';
if (tab === 'cenas') {
if (btnTabHistoriaCenas) {
btnTabHistoriaCenas.classList.add('active');
btnTabHistoriaCenas.style.background = 'rgba(16,185,129,0.15)';
btnTabHistoriaCenas.style.color = '#34d399';
}
if (historiaCenasView) historiaCenasView.style.display = 'flex';
} else if (tab === 'texto') {
if (btnTabHistoriaTexto) {
btnTabHistoriaTexto.classList.add('active');
btnTabHistoriaTexto.style.background = 'rgba(16,185,129,0.15)';
btnTabHistoriaTexto.style.color = '#34d399';
}
if (historiaTextoView) historiaTextoView.style.display = 'block';
} else if (tab === 'atividades') {
if (btnTabHistoriaAtividades) {
btnTabHistoriaAtividades.classList.add('active');
btnTabHistoriaAtividades.style.background = 'rgba(16,185,129,0.15)';
btnTabHistoriaAtividades.style.color = '#34d399';
}
if (historiaAtividadesView) historiaAtividadesView.style.display = 'flex';
}
}
if (btnTabHistoriaCenas) btnTabHistoriaCenas.addEventListener('click', () => switchHistoriaTab('cenas'));
if (btnTabHistoriaTexto) btnTabHistoriaTexto.addEventListener('click', () => switchHistoriaTab('texto'));
if (btnTabHistoriaAtividades) btnTabHistoriaAtividades.addEventListener('click', () => switchHistoriaTab('atividades'));
// Renderizar Cena Atual
function renderCurrentScene() {
if (!currentHistoriaData || !currentHistoriaData.cenas || currentHistoriaData.cenas.length === 0) return;
const total = currentHistoriaData.cenas.length;
if (currentSceneIndex < 0) currentSceneIndex = 0;
if (currentSceneIndex >= total) currentSceneIndex = total - 1;
const cena = currentHistoriaData.cenas[currentSceneIndex];
if (historiaCenaIndicator) historiaCenaIndicator.textContent = `Cena ${currentSceneIndex + 1} de ${total}`;
if (historiaCenaTitle) historiaCenaTitle.textContent = cena.titulo || `Ato ${currentSceneIndex + 1}`;
if (historiaCenaText) historiaCenaText.textContent = cena.texto || '';
// Imagem da cena
if (cena.imageUrl) {
if (historiaCenaImg) historiaCenaImg.src = cena.imageUrl;
if (historiaCenaImageContainer) historiaCenaImageContainer.style.display = 'block';
if (btnGenerateSceneImage) btnGenerateSceneImage.innerHTML = '🔄 Regenerar Ilustração';
} else {
if (historiaCenaImageContainer) historiaCenaImageContainer.style.display = 'none';
if (btnGenerateSceneImage) btnGenerateSceneImage.innerHTML = '🎨 Gerar Ilustração';
}
if (btnPrevCena) btnPrevCena.disabled = currentSceneIndex === 0;
if (btnNextCena) btnNextCena.disabled = currentSceneIndex === total - 1;
}
if (btnPrevCena) {
btnPrevCena.addEventListener('click', () => {
if (currentSceneIndex > 0) {
currentSceneIndex--;
renderCurrentScene();
}
});
}
if (btnNextCena) {
btnNextCena.addEventListener('click', () => {
if (currentHistoriaData && currentHistoriaData.cenas && currentSceneIndex < currentHistoriaData.cenas.length - 1) {
currentSceneIndex++;
renderCurrentScene();
}
});
}
// Gerar Ilustração da Cena Atual
if (btnGenerateSceneImage) {
btnGenerateSceneImage.addEventListener('click', async () => {
if (!currentHistoriaData || !currentHistoriaData.cenas) return;
const cena = currentHistoriaData.cenas[currentSceneIndex];
if (!cena) return;
const originalBtnHtml = btnGenerateSceneImage.innerHTML;
btnGenerateSceneImage.disabled = true;
btnGenerateSceneImage.innerHTML = '⏳ Ilustrando...';
try {
const res = await fetch('/api/historias/scene-image', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: cena.promptImagem || cena.texto || 'Cute children book illustration',
estiloVisual: currentHistoriaData.estiloVisual || 'Cartoon colorido',
cenaNumero: currentSceneIndex + 1
})
});
const data = await res.json();
if (res.ok && data.imageUrl) {
cena.imageUrl = data.imageUrl;
renderCurrentScene();
showToast('Ilustração da cena criada com sucesso!', 'success');
} else {
showToast(data.error || 'Erro ao gerar ilustração.', 'error');
}
} catch (err) {
showToast('Erro ao conectar com gerador de ilustração.', 'error');
} finally {
btnGenerateSceneImage.disabled = false;
btnGenerateSceneImage.innerHTML = originalBtnHtml;
}
});
}
// Gerar História Completa
if (btnGenerateHistoria) { if (btnGenerateHistoria) {
btnGenerateHistoria.addEventListener('click', async () => { btnGenerateHistoria.addEventListener('click', async () => {
const tema = historiaTemaInput.value.trim(); const tema = historiaTemaInput.value.trim();
if (!tema) { if (!tema) {
showToast('Por favor, informe o tema da história.', 'error'); showToast('Por favor, informe o tema ou enredo da história.', 'error');
return; return;
} }
btnGenerateHistoria.disabled = true; btnGenerateHistoria.disabled = true;
historiaResultArea.style.display = 'none'; if (historiaGenerateLoader) historiaGenerateLoader.style.display = 'flex';
historiaGenerateLoader.style.display = 'flex'; if (historiaPlaceholderText) historiaPlaceholderText.style.display = 'none';
try { try {
const response = await fetch('/api/historias/generate', { const response = await fetch('/api/historias/generate', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
faixaEtaria: historiaFaixaEtaria.value, tema: tema,
paragrafos: historiaParagrafos.value, faixaEtaria: historiaFaixaEtaria ? historiaFaixaEtaria.value : 'Crianças pequenas',
personagens: historiaPersonagens.value, paragrafos: historiaParagrafos ? historiaParagrafos.value : '3',
estilo: historiaEstilo.value, estilo: historiaEstilo ? historiaEstilo.value : 'Lúdico',
tema: tema estiloVisual: historiaEstiloVisual ? historiaEstiloVisual.value : 'Cartoon colorido',
personagemPrincipal: historiaPersonagemInput ? historiaPersonagemInput.value.trim() : '',
objetoMagico: historiaObjetoMagicoInput ? historiaObjetoMagicoInput.value.trim() : '',
turmaId: historiaTurmaSelect ? historiaTurmaSelect.value : null
}) })
}); });
const data = await response.json(); const data = await response.json();
if (response.ok) { if (response.ok && data.success) {
currentHistoria = data.historia; currentHistoriaData = data;
historiaTextOutput.innerHTML = window.marked ? marked.parse(currentHistoria) : currentHistoria; currentSceneIndex = 0;
historiaResultArea.style.display = 'flex';
// Header
if (historiaActiveTitle) historiaActiveTitle.textContent = data.titulo || 'História Mágica';
if (historiaActiveSubtitle) {
historiaActiveSubtitle.textContent = `Personagem: ${data.personagemPrincipal || 'Turma'}${data.cenas ? data.cenas.length : 3} Cenas • ${data.duracaoLeitura || '3 min'}`;
}
if (historiaBnccBadge) {
historiaBnccBadge.textContent = `BNCC: ${data.bnccCampos || 'EI02EF04 / EI03EF05'}`;
historiaBnccBadge.style.display = 'inline-block';
}
// Player de Áudio
if (data.audioUrl) {
if (historiaAudioPlayer) historiaAudioPlayer.src = data.audioUrl;
if (btnDownloadHistoriaAudio) btnDownloadHistoriaAudio.href = data.audioUrl;
if (historiaAudioPlayerBox) historiaAudioPlayerBox.style.display = 'flex';
if (historiaAudioPlayer) {
historiaAudioPlayer.onplay = () => historiaWaveBars.forEach(b => b.classList.add('wave-active'));
historiaAudioPlayer.onpause = () => historiaWaveBars.forEach(b => b.classList.remove('wave-active'));
historiaAudioPlayer.onended = () => historiaWaveBars.forEach(b => b.classList.remove('wave-active'));
}
} else {
if (historiaAudioPlayerBox) historiaAudioPlayerBox.style.display = 'none';
}
// Abas & Visualização
if (historiaContentTabs) historiaContentTabs.style.display = 'flex';
if (historiaActiveBox) historiaActiveBox.style.display = 'flex';
// Texto completo
if (historiaTextoView) historiaTextoView.textContent = data.historiaCompleta || '';
// Atividades e BNCC
if (historiaBnccDesc) historiaBnccDesc.textContent = data.bnccCampos || 'EI02EF04 / EI03EF05 • Escuta, Fala, Pensamento e Imaginação';
if (historiaPerguntasList) {
historiaPerguntasList.innerHTML = '';
(data.perguntasCompreensao || []).forEach(p => {
const pEl = document.createElement('div');
pEl.style.cssText = 'padding: 4px 0; border-bottom: 1px dashed rgba(255,255,255,0.05);';
pEl.textContent = p;
historiaPerguntasList.appendChild(pEl);
});
}
renderCurrentScene();
switchHistoriaTab('cenas');
showToast('História inventada com sucesso!', 'success');
} else { } else {
showToast(data.error || 'Erro ao inventar história.', 'error'); showToast(data.error || 'Erro ao gerar história.', 'error');
} }
} catch (err) { } catch (err) {
showToast('Erro de conexão.', 'error'); showToast('Erro de conexão ao gerar história.', 'error');
} finally { } finally {
historiaGenerateLoader.style.display = 'none'; if (historiaGenerateLoader) historiaGenerateLoader.style.display = 'none';
btnGenerateHistoria.disabled = false; btnGenerateHistoria.disabled = false;
} }
}); });
} }
if (btnCopyHistoria) { // Copiar História
btnCopyHistoria.addEventListener('click', () => { if (btnCopyActiveHistoria) {
if (currentHistoria) { btnCopyActiveHistoria.addEventListener('click', () => {
navigator.clipboard.writeText(currentHistoria); if (currentHistoriaData && currentHistoriaData.historiaCompleta) {
navigator.clipboard.writeText(currentHistoriaData.historiaCompleta);
showToast('História copiada para a área de transferência!', 'success'); showToast('História copiada para a área de transferência!', 'success');
} }
}); });
} }
if (btnDownloadHistoriaPdf) { // Baixar TXT
btnDownloadHistoriaPdf.addEventListener('click', () => { if (btnDownloadActiveHistoriaTxt) {
if (!currentHistoria) return; btnDownloadActiveHistoriaTxt.addEventListener('click', () => {
const printWindow = window.open('', '_blank'); if (!currentHistoriaData) return;
printWindow.document.write(` const content = `=========================================
<html> 📖 ${currentHistoriaData.titulo || 'HISTÓRIA PEDAGÓGICA'}
=========================================
Personagem Principal: ${currentHistoriaData.personagemPrincipal || 'Turma'}
Objeto Mágico: ${currentHistoriaData.objetoMagico || 'Especial'}
BNCC: ${currentHistoriaData.bnccCampos || 'EI02EF04'}
Duração Estimada: ${currentHistoriaData.duracaoLeitura || '3 min'}
--- HISTÓRIA COMPLETA ---
${currentHistoriaData.historiaCompleta || ''}
--- PERGUNTAS PARA RODA DE CONVERSA ---
${(currentHistoriaData.perguntasCompreensao || []).join('\n')}
Gerado por PedagogIA Assistente da Professora Camila`;
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${(currentHistoriaData.titulo || 'historia').toLowerCase().replace(/\s+/g, '_')}.txt`;
a.click();
URL.revokeObjectURL(url);
});
}
// Musicar História
if (btnSendHistoriaToMusic) {
btnSendHistoriaToMusic.addEventListener('click', () => {
if (!currentHistoriaData) return;
const barBtnMusica = document.getElementById('barBtnMusica');
const musicaTemaInput = document.getElementById('musicaTemaInput');
historiaModal.style.display = 'none';
if (barBtnMusica) barBtnMusica.click();
if (musicaTemaInput) {
musicaTemaInput.value = `Canção sobre a história "${currentHistoriaData.titulo}": ${currentHistoriaData.objetoMagico || ''}, amizade e imaginação`;
}
showToast('História transferida para o Musicando Ideias!', 'info');
});
}
// Exportar PDF Ilustrado
if (btnExportActiveHistoriaPdf) {
btnExportActiveHistoriaPdf.addEventListener('click', () => {
if (!currentHistoriaData) return;
const printWin = window.open('', '_blank');
if (!printWin) {
showToast('Permita pop-ups para gerar o PDF.', 'warning');
return;
}
let cenasHtml = '';
(currentHistoriaData.cenas || []).forEach((c, idx) => {
cenasHtml += `
<div style="margin-bottom: 20px; padding: 15px; border-left: 4px solid #10b981; background: #f8fafc; border-radius: 0 8px 8px 0;">
<h3 style="margin-top: 0; color: #059669; font-size: 1.1rem;">${c.titulo || `Ato ${idx + 1}`}</h3>
${c.imageUrl ? `<div style="text-align: center; margin: 10px 0;"><img src="${c.imageUrl}" style="max-height: 220px; border-radius: 8px;" /></div>` : ''}
<p style="font-size: 1rem; line-height: 1.7; color: #334155; margin: 0;">${c.texto || ''}</p>
</div>
`;
});
let perguntasHtml = '';
(currentHistoriaData.perguntasCompreensao || []).forEach(p => {
perguntasHtml += `<li style="margin-bottom: 6px;">${p}</li>`;
});
printWin.document.write(`
<!DOCTYPE html>
<html lang="pt-BR">
<head> <head>
<title>História - Assistente PedaGog</title> <meta charset="UTF-8">
<title>${currentHistoriaData.titulo || 'História Pedagógica'} - Pedagog</title>
<style> <style>
body { font-family: 'Outfit', 'Inter', sans-serif; padding: 40px; color: #333; line-height: 1.6; } body { font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; padding: 35px; color: #1e293b; line-height: 1.6; max-width: 800px; margin: 0 auto; }
h1, h2, h3 { color: #10b981; margin-top: 24px; margin-bottom: 12px; } h1 { color: #059669; margin: 0 0 6px 0; font-size: 1.8rem; }
@media print { body { padding: 0; } button { display: none; } } .header-box { border-bottom: 2px solid #e2e8f0; padding-bottom: 14px; margin-bottom: 24px; }
.badge { display: inline-block; background: #d1fae5; color: #065f46; font-size: 0.8rem; padding: 4px 10px; border-radius: 6px; font-weight: bold; margin-right: 6px; }
.btn-print { background: #10b981; color: white; border: none; padding: 10px 20px; border-radius: 8px; font-weight: bold; cursor: pointer; float: right; font-size: 0.95rem; }
@media print { .btn-print { display: none; } body { padding: 0; } }
</style> </style>
</head> </head>
<body> <body>
<div style="max-width: 800px; margin: 0 auto;"> <button class="btn-print" onclick="window.print()">🖨 Imprimir / Salvar PDF</button>
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; margin-bottom: 20px;"> <div class="header-box">
<h1 style="margin: 0;"> História Pedagógica</h1> <h1>📖 ${currentHistoriaData.titulo || 'História Pedagógica'}</h1>
<button onclick="window.print()" style="background: #10b981; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: bold;">Imprimir / PDF</button> <div style="margin-top: 8px;">
<span class="badge">Personagem: ${currentHistoriaData.personagemPrincipal || 'Turma'}</span>
<span class="badge">Objeto: ${currentHistoriaData.objetoMagico || 'Especial'}</span>
<span class="badge">BNCC: ${currentHistoriaData.bnccCampos || 'EI02EF04'}</span>
</div> </div>
<div>${window.marked ? marked.parse(currentHistoria) : currentHistoria}</div> </div>
<h2 style="color: #0f172a; font-size: 1.2rem; border-bottom: 1px solid #cbd5e1; padding-bottom: 6px;">🎬 Cenas da História</h2>
${cenasHtml}
<div style="margin-top: 30px; padding: 18px; background: #f1f5f9; border-radius: 8px;">
<h3 style="margin-top: 0; color: #0f172a; font-size: 1.05rem;">🧩 Roda de Conversa & Atividades</h3>
<ul style="margin: 0; padding-left: 20px; color: #475569; font-size: 0.95rem;">
${perguntasHtml}
</ul>
</div>
<div style="margin-top: 40px; text-align: center; font-size: 0.8rem; color: #94a3b8; border-top: 1px solid #e2e8f0; padding-top: 12px;">
Pedagog Estúdio de Literatura Infantil da Professora Camila Martella Gasparini Reifonas
</div> </div>
</body> </body>
</html> </html>
`); `);
printWindow.document.close(); printWin.document.close();
}); });
} }
// Carregar Histórico
async function loadHistoriaHistory() { async function loadHistoriaHistory() {
const listContainer = document.getElementById('historiaHistoryList'); const listContainer = document.getElementById('historiaHistoryList');
if (!listContainer) return; if (!listContainer) return;
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando...</p>'; listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Carregando histórias...</p>';
try { try {
const response = await fetch('/api/historias/list'); const response = await fetch('/api/historias/list');
const data = await response.json(); const data = await response.json();
@@ -7835,19 +8242,24 @@ window.toggleCustomAudio = (btn) => {
const date = new Date(item.created_at).toLocaleString('pt-BR'); const date = new Date(item.created_at).toLocaleString('pt-BR');
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'history-item-card'; div.className = 'history-item-card';
div.style.cssText = 'background: rgba(255,255,255,0.02); padding: 14px; border-radius: 8px; border: 1px solid var(--border-light); display: flex; flex-direction: column; gap: 8px; cursor: pointer; position: relative;'; div.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: 6px; cursor: pointer; position: relative;';
div.innerHTML = ` div.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: flex-start; padding-right: 30px;"> <div style="display: flex; justify-content: space-between; align-items: flex-start; padding-right: 30px;">
<strong style="color: var(--brand-green); font-size: 0.95rem;">${item.faixa_etaria} (${item.estilo})</strong> <strong style="color: #34d399; font-size: 0.95rem;">${item.titulo || item.tema || 'História'}</strong>
<span style="font-size: 0.75rem; color: var(--text-secondary);">${date}</span> <span style="font-size: 0.72rem; color: var(--text-secondary);">${date}</span>
</div>
<p style="margin: 0; font-size: 0.82rem; color: var(--text-primary); opacity: 0.9;">Tema: ${item.tema}</p>
<div style="display: flex; gap: 8px; font-size: 0.72rem; color: var(--text-secondary);">
<span>👶 ${item.faixa_etaria}</span>
${item.personagem_principal ? `<span>• 👤 ${item.personagem_principal}</span>` : ''}
${item.audio_url ? `<span>• 🎙️ Áudio MP3</span>` : ''}
</div> </div>
<p style="margin: 0; font-size: 0.85rem; color: var(--text-primary); opacity: 0.9;">Tema: ${item.tema}</p>
<button class="btn-delete-historia" data-id="${item.id}" style="position: absolute; top: 12px; right: 12px; background: transparent; border: none; color: #ef4444; cursor: pointer; font-size: 1.1rem; padding: 4px; border-radius: 4px;" title="Excluir">🗑</button> <button class="btn-delete-historia" data-id="${item.id}" style="position: absolute; top: 12px; right: 12px; background: transparent; border: none; color: #ef4444; cursor: pointer; font-size: 1.1rem; padding: 4px; border-radius: 4px;" title="Excluir">🗑</button>
`; `;
div.addEventListener('click', async (e) => { div.addEventListener('click', async (e) => {
if (e.target.closest('.btn-delete-historia')) { if (e.target.closest('.btn-delete-historia')) {
e.stopPropagation(); e.stopPropagation();
if (confirm('Tem certeza que deseja excluir?')) { if (confirm('Tem certeza que deseja excluir esta história?')) {
try { try {
const delRes = await fetch(`/api/historias/${item.id}`, { method: 'DELETE' }); const delRes = await fetch(`/api/historias/${item.id}`, { method: 'DELETE' });
if (delRes.ok) { showToast('História excluída.', 'success'); loadHistoriaHistory(); } if (delRes.ok) { showToast('História excluída.', 'success'); loadHistoriaHistory(); }
@@ -7859,19 +8271,69 @@ window.toggleCustomAudio = (btn) => {
const res = await fetch(`/api/historias/${item.id}`); const res = await fetch(`/api/historias/${item.id}`);
const detail = await res.json(); const detail = await res.json();
if (res.ok) { if (res.ok) {
currentHistoria = detail.historia; currentHistoriaData = {
historiaTextOutput.innerHTML = window.marked ? marked.parse(currentHistoria) : currentHistoria; id: detail.id,
historiaResultArea.style.display = 'flex'; titulo: detail.titulo || `História: ${detail.tema}`,
personagemPrincipal: detail.personagem_principal || '',
objetoMagico: detail.objeto_magico || '',
historiaCompleta: detail.historia || '',
cenas: typeof detail.cenas === 'string' ? JSON.parse(detail.cenas) : (detail.cenas || []),
perguntasCompreensao: typeof detail.perguntas_compreensao === 'string' ? JSON.parse(detail.perguntas_compreensao) : (detail.perguntas_compreensao || []),
bnccCampos: detail.bncc || 'EI02EF04 / EI03EF05',
duracaoLeitura: detail.duracao_leitura || '3 min',
complexidade: detail.complexidade || 'Lúdica',
audioUrl: detail.audio_url || null,
estiloVisual: detail.estilo_visual || 'Cartoon colorido'
};
currentSceneIndex = 0;
if (historiaActiveTitle) historiaActiveTitle.textContent = currentHistoriaData.titulo;
if (historiaActiveSubtitle) {
historiaActiveSubtitle.textContent = `Personagem: ${currentHistoriaData.personagemPrincipal || 'Turma'}${currentHistoriaData.cenas ? currentHistoriaData.cenas.length : 3} Cenas`;
}
if (historiaBnccBadge) {
historiaBnccBadge.textContent = `BNCC: ${currentHistoriaData.bnccCampos}`;
historiaBnccBadge.style.display = 'inline-block';
}
if (currentHistoriaData.audioUrl) {
if (historiaAudioPlayer) historiaAudioPlayer.src = currentHistoriaData.audioUrl;
if (btnDownloadHistoriaAudio) btnDownloadHistoriaAudio.href = currentHistoriaData.audioUrl;
if (historiaAudioPlayerBox) historiaAudioPlayerBox.style.display = 'flex';
} else {
if (historiaAudioPlayerBox) historiaAudioPlayerBox.style.display = 'none';
}
if (historiaContentTabs) historiaContentTabs.style.display = 'flex';
if (historiaActiveBox) historiaActiveBox.style.display = 'flex';
if (historiaPlaceholderText) historiaPlaceholderText.style.display = 'none';
if (historiaTextoView) historiaTextoView.textContent = currentHistoriaData.historiaCompleta;
if (historiaBnccDesc) historiaBnccDesc.textContent = currentHistoriaData.bnccCampos;
if (historiaPerguntasList) {
historiaPerguntasList.innerHTML = '';
(currentHistoriaData.perguntasCompreensao || []).forEach(p => {
const pEl = document.createElement('div');
pEl.style.cssText = 'padding: 4px 0; border-bottom: 1px dashed rgba(255,255,255,0.05);';
pEl.textContent = p;
historiaPerguntasList.appendChild(pEl);
});
}
renderCurrentScene();
switchHistoriaTab('cenas');
historiaHistoryModal.style.display = 'none'; historiaHistoryModal.style.display = 'none';
} }
} catch (err) { showToast('Erro ao carregar.', 'error'); } } catch (err) { showToast('Erro ao carregar história.', 'error'); }
}); });
listContainer.appendChild(div); listContainer.appendChild(div);
}); });
} else { } else {
listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Nenhum histórico.</p>'; listContainer.innerHTML = '<p style="text-align:center; color:var(--text-secondary);">Nenhuma história salva ainda.</p>';
} }
} catch (err) { listContainer.innerHTML = '<p style="text-align:center; color:#ef4444;">Erro ao carregar.</p>'; } } catch (err) {
listContainer.innerHTML = '<p style="text-align:center; color:#ef4444;">Erro ao carregar histórico de histórias.</p>';
}
} }
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
+250 -64
View File
@@ -1208,88 +1208,274 @@
</div> </div>
<!-- ========================================== --> <!-- ========================================== -->
<!-- MODAL: INVENTANDO HISTORIAS --> <!-- MODAL: INVENTANDO HISTÓRIAS (ESTÚDIO PROFISSIONAL) -->
<!-- ========================================== --> <!-- ========================================== -->
<div id="historiaModal" class="settings-modal" style="display: none;"> <div id="historiaModal" 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-content" style="max-width: 1080px; width: 95%; max-height: 92vh; 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;">✍️ Inventando Histórias PedaGog</h3>
<button id="btnCloseHistoriaModal" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem;">&times;</button>
</div>
<div style="padding: 12px 20px; display: flex; justify-content: flex-end; border-bottom: 1px solid var(--border-light);"> <!-- Header -->
<button id="btnOpenHistoriaHistory" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-primary); padding: 6px 12px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; gap: 6px; font-size: 0.85rem;">🕒 Ver Histórico</button> <div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light); padding: 14px 20px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
</div> <div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 1.4rem;">📖</span>
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 18px; flex: 1;"> <div>
<h3 style="margin: 0; font-family: 'Outfit', sans-serif; font-size: 1.15rem; color: var(--text-primary);">Inventando Histórias • Estúdio PedagogIA</h3>
<div class="settings-group"> <span style="font-size: 0.75rem; color: var(--text-secondary);">Narrativa em 3 atos, personagens da turma, ilustrações por cena e áudio narrado</span>
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">1. Faixa Etária</label> </div>
<select id="historiaFaixaEtaria" 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>
<div style="display: flex; gap: 8px; align-items: center;">
<button type="button" id="btnOpenHistoriaHistory" style="background: rgba(16, 185, 129, 0.12); border: 1px solid rgba(16, 185, 129, 0.3); color: #34d399; padding: 6px 12px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; gap: 6px; font-size: 0.82rem; font-weight: 600;">🕒 Histórico</button>
<button type="button" id="btnCloseHistoriaModal" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem; line-height: 1;">&times;</button>
</div>
</div>
<div style="display: flex; gap: 12px; align-items: flex-start;"> <!-- Body em 2 Colunas -->
<div class="settings-group" style="flex: 1;"> <div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: row; gap: 24px; flex: 1; flex-wrap: wrap;">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Tamanho (Parágrafos)</label>
<select id="historiaParagrafos" class="obs-select" style="width: 100%; margin-top: 4px;"> <!-- Lado Esquerdo: Criação & Parâmetros Narrativos -->
<option value="2">2 Parágrafos</option> <div id="historiaLeftPanel" style="flex: 1; min-width: 320px; display: flex; flex-direction: column; gap: 14px;">
<option value="3" selected>3 Parágrafos</option>
<option value="5">5 Parágrafos</option> <h4 style="margin: 0; color: var(--text-primary); border-bottom: 1px dashed var(--border-light); padding-bottom: 6px; font-size: 0.92rem;">✨ 1. Inspiração & Personagens</h4>
<option value="7">7 Parágrafos</option>
<!-- TEMPLATES RÁPIDOS DE HISTÓRIA -->
<div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Modelos de História (Templates)</label>
<select id="historiaTemplateSelect" class="obs-select" style="width: 100%; margin-top: 4px; color: #10b981; font-weight: 600;">
<option value="">✨ Escolha um modelo de história...</option>
<option value="arca">📦 A Arca da Imaginação e as Descobertas</option>
<option value="horta">🌱 O Mistério da Horta Encantada</option>
<option value="dinossauro">🦖 O Pequeno Dinossauro que Queria Amigos</option>
<option value="chuva">🌧️ O Dia em que a Chuva Dançou no Parque</option>
<option value="bolhas">🧼 O Reino Mágico das Bolhas de Sabão</option>
<option value="brinquedos">🧸 A Grande Aventura dos Brinquedos Perdidos</option>
<option value="emocoes">💖 O Monstrinho das Cores e dos Sentimentos</option>
</select> </select>
</div> </div>
<div class="settings-group" style="flex: 1;">
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">Personagens</label> <!-- TURMA & PERSONAGEM PRINCIPAL -->
<input type="number" id="historiaPersonagens" value="2" min="1" max="10" class="obs-input" style="width: 100%; margin-top: 4px; padding: 10px;"> <div style="display: flex; gap: 10px; flex-wrap: wrap;">
<div class="settings-group" style="flex: 1; min-width: 140px;">
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Vincular à Turma</label>
<select id="historiaTurmaSelect" class="obs-select" style="width: 100%; margin-top: 4px;">
<option value="">-- Selecionar Turma --</option>
</select>
</div>
<div class="settings-group" style="flex: 1; min-width: 140px;">
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Personagem da Turma</label>
<input type="text" id="historiaPersonagemInput" placeholder="Ex: João, Maria, toda a turma..." class="obs-input" style="width: 100%; margin-top: 4px; padding: 8px;">
</div>
</div> </div>
</div>
<div class="settings-group"> <!-- OBJETO MÁGICO / ELEMENTO DISPARADOR -->
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">3. Estilo da Narrativa</label> <div class="settings-group">
<select id="historiaEstilo" class="obs-select" style="width: 100%; margin-top: 4px;"> <label style="color: var(--text-secondary); font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Objeto Mágico ou Elemento Disparador</label>
<option value="Narração Direta">Narração Direta (Apenas Narrador)</option> <input type="text" id="historiaObjetoMagicoInput" value="Arca da Imaginação" placeholder="Ex: Arca mágica, semente brilhante, lupa encantada..." class="obs-input" style="width: 100%; margin-top: 4px; padding: 8px;">
<option value="Dialogada">Dialogada (Com fala dos personagens)</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;">4. Tema da História</label>
<input type="text" id="historiaTemaInput" placeholder="Ex: Uma fada que não sabia voar, a importância de dividir..." class="obs-input" style="width: 100%; margin-top: 4px; padding: 10px;">
</div>
<button type="button" id="btnGenerateHistoria" style="width: 100%; padding: 12px; background: #10a37f; 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, 163, 127, 0.3); transition: all 0.2s;">✍️ Inventar História</button>
<div id="historiaGenerateLoader" style="display: none; align-items: center; justify-content: center; gap: 10px; margin-top: 15px;">
<div class="spinner"></div>
<span style="color: var(--text-secondary); font-size: 0.9rem;">Inventando a história...</span>
</div>
<div id="historiaResultArea" style="display: none; flex-direction: column; gap: 12px; margin-top: 20px;">
<div style="background: rgba(16, 185, 129, 0.05); border: 1px solid rgba(16, 185, 129, 0.2); padding: 15px; border-radius: 8px;">
<h4 style="color: var(--brand-green); margin-top: 0; margin-bottom: 10px;">História Gerada:</h4>
<div id="historiaTextOutput" style="font-size: 0.95rem; line-height: 1.6; white-space: pre-wrap; color: var(--text-primary);"></div>
</div> </div>
<div style="display: flex; gap: 10px; justify-content: flex-end;">
<button id="btnCopyHistoria" style="background: var(--bg-secondary); border: 1px solid var(--border-light); color: var(--text-primary); padding: 8px 16px; border-radius: 6px; cursor: pointer;">📋 Copiar</button> <!-- TEMA / ENREDO -->
<button id="btnDownloadHistoriaPdf" style="background: var(--brand-green); border: none; color: white; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: bold;">📄 Salvar PDF</button> <div class="settings-group">
<label style="color: var(--text-secondary); font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Tema / Enredo da História</label>
<textarea id="historiaTemaInput" placeholder="Ex: Crianças encontram uma arca mágica no quintal da escola e precisam cooperar para descobrir os segredos..." class="obs-input" style="width: 100%; min-height: 55px; padding: 8px; margin-top: 4px; font-size: 0.85rem; resize: vertical;"></textarea>
</div> </div>
<h4 style="margin: 0; color: var(--text-primary); border-bottom: 1px dashed var(--border-light); padding-bottom: 6px; margin-top: 4px; font-size: 0.92rem;">🏗️ 2. Estrutura Narrativa & Estilo Visual</h4>
<!-- IDADE & ESTRUTURA DOS ATOS -->
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<div class="settings-group" style="flex: 1; min-width: 140px;">
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Faixa Etária</label>
<select id="historiaFaixaEtaria" class="obs-select" style="width: 100%; margin-top: 4px;">
<option value="Bebês (0 a 1 ano e 6 meses)">Bebês (0 a 1a6m)</option>
<option value="Crianças bem pequenas (1 ano e 7 meses a 3 anos e 11 meses)">Bem pequenas (1a7m a 3a11m)</option>
<option value="Crianças pequenas (4 anos a 5 anos e 11 meses)" selected>Crianças pequenas (4 a 5a11m)</option>
<option value="Ensino Fundamental I (6 a 10 anos)">Ensino Fundamental I</option>
</select>
</div>
<div class="settings-group" style="flex: 1; min-width: 140px;">
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Cenas / Atos</label>
<select id="historiaParagrafos" class="obs-select" style="width: 100%; margin-top: 4px;">
<option value="3" selected>🎬 3 Cenas (Ato 1, Ato 2, Ato 3)</option>
<option value="4">🎬 4 Cenas Narrativas</option>
<option value="5">🎬 5 Cenas Detalhadas</option>
</select>
</div>
</div>
<!-- ESTILO NARRATIVO & ESTILO VISUAL -->
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<div class="settings-group" style="flex: 1; min-width: 140px;">
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Estilo Narrativo</label>
<select id="historiaEstilo" class="obs-select" style="width: 100%; margin-top: 4px;">
<option value="Lúdico, Afetivo e Dialogado" selected>💬 Lúdico & Dialogado</option>
<option value="Contação Clássica com Suspense Leve">📖 Contação Clássica</option>
<option value="Roda de Conversa Interativa">🗣️ Interativo para Roda</option>
<option value="Aventura e Descoberta na Escola">🚀 Aventura & Cooperação</option>
</select>
</div>
<div class="settings-group" style="flex: 1; min-width: 140px;">
<label style="color: var(--text-secondary); font-size: 0.72rem; font-weight: 600; text-transform: uppercase;">Estilo das Ilustrações</label>
<select id="historiaEstiloVisual" class="obs-select" style="width: 100%; margin-top: 4px;">
<option value="Cartoon colorido e acolhedor" selected>🎨 Cartoon Colorido</option>
<option value="Aquarela suave infantil">🖌️ Aquarela Suave</option>
<option value="Livro infantil clássico e detalhado">📚 Livro Infantil Clássico</option>
<option value="Giz de cera e textura lúdica">🖍️ Giz de Cera Infantil</option>
</select>
</div>
</div>
<!-- BOTÃO GERAR HISTÓRIA -->
<button type="button" id="btnGenerateHistoria" style="width: 100%; padding: 12px; background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; border: none; border-radius: 10px; font-weight: 700; font-size: 0.95rem; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; box-shadow: 0 4px 14px rgba(16, 185, 129, 0.3); transition: all 0.2s; margin-top: 6px;">
<span>📖 Inventar História Mágica (3 Atos + Cenas)</span>
</button>
<div id="historiaGenerateLoader" style="display: none; align-items: center; justify-content: center; gap: 10px; color: var(--text-secondary); font-size: 0.85rem; padding: 10px; background: rgba(255, 255, 255, 0.02); border-radius: 8px;">
<div class="media-spinner" style="width: 18px; height: 18px; border-width: 2px; border-top-color: #10b981;"></div>
<span id="historiaLoaderText">Construindo narrativa em 3 atos e áudio...</span>
</div>
</div> </div>
<!-- Lado Direito: Visualizador de Cenas, Ilustrações e Leitor -->
<div id="historiaRightPanel" 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: 6px;">
<h4 style="margin: 0; color: var(--text-primary); font-size: 0.95rem;">📚 Livro de Histórias & Cenas</h4>
<div id="historiaBnccBadge" style="display: none; font-size: 0.72rem; background: rgba(16, 185, 129, 0.15); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.3); padding: 3px 8px; border-radius: 6px; font-weight: 600;">
BNCC: EI02EF04
</div>
</div>
<!-- ÁREA DO PLAYER DE ÁUDIO NARRADO -->
<div id="historiaAudioPlayerBox" style="display: none; background: linear-gradient(135deg, rgba(16,185,129,0.12), rgba(59,130,246,0.08)); border: 1px solid rgba(16,185,129,0.3); border-radius: 12px; padding: 14px; flex-direction: column; gap: 10px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.2rem;">🎙️</span>
<div>
<div style="font-size: 0.85rem; font-weight: 700; color: var(--text-primary);" id="historiaPlayerTitle">Narração por PedagogIA</div>
<div style="font-size: 0.72rem; color: var(--text-secondary);">Contação de Histórias • Formato MP3</div>
</div>
</div>
<div style="display: flex; gap: 6px; align-items: center;">
<a id="btnDownloadHistoriaAudio" href="#" download="historia_narrada.mp3" style="background: #10b981; color: white; border: none; padding: 5px 10px; border-radius: 6px; font-size: 0.75rem; font-weight: 700; text-decoration: none; display: flex; align-items: center; gap: 4px;">
📥 Baixar Narração
</a>
</div>
</div>
<!-- Waveform / Equalizer Animado -->
<div id="historiaWaveformVisualizer" style="display: flex; align-items: center; justify-content: center; gap: 3px; height: 28px; padding: 4px; background: rgba(0,0,0,0.15); border-radius: 6px;">
<div class="wave-bar wave-bar-historia" style="width: 4px; height: 30%; background: #10b981; border-radius: 2px;"></div>
<div class="wave-bar wave-bar-historia" style="width: 4px; height: 60%; background: #10b981; border-radius: 2px;"></div>
<div class="wave-bar wave-bar-historia" style="width: 4px; height: 90%; background: #10b981; border-radius: 2px;"></div>
<div class="wave-bar wave-bar-historia" style="width: 4px; height: 45%; background: #10b981; border-radius: 2px;"></div>
<div class="wave-bar wave-bar-historia" style="width: 4px; height: 75%; background: #10b981; border-radius: 2px;"></div>
<div class="wave-bar wave-bar-historia" style="width: 4px; height: 35%; background: #10b981; border-radius: 2px;"></div>
<div class="wave-bar wave-bar-historia" style="width: 4px; height: 85%; background: #10b981; border-radius: 2px;"></div>
<div class="wave-bar wave-bar-historia" style="width: 4px; height: 50%; background: #10b981; border-radius: 2px;"></div>
<div class="wave-bar wave-bar-historia" style="width: 4px; height: 95%; background: #10b981; border-radius: 2px;"></div>
<div class="wave-bar wave-bar-historia" style="width: 4px; height: 40%; background: #10b981; border-radius: 2px;"></div>
</div>
<audio id="historiaAudioPlayer" controls style="width: 100%; margin-top: 2px;"></audio>
</div>
<!-- ABAS DE NAVEGAÇÃO -->
<div id="historiaContentTabs" style="display: none; gap: 6px; border-bottom: 1px solid var(--border-light); padding-bottom: 8px;">
<button type="button" id="btnTabHistoriaCenas" class="tab-btn active" style="font-size: 0.78rem; padding: 6px 12px; background: rgba(16,185,129,0.15); border: none; cursor: pointer; color: #34d399; border-radius: 6px; font-weight: bold;">🎬 Cenas & Ilustrações</button>
<button type="button" id="btnTabHistoriaTexto" class="tab-btn" style="font-size: 0.78rem; padding: 6px 12px; background: transparent; border: none; cursor: pointer; color: var(--text-secondary); border-radius: 6px;">📖 História Completa</button>
<button type="button" id="btnTabHistoriaAtividades" class="tab-btn" style="font-size: 0.78rem; padding: 6px 12px; background: transparent; border: none; cursor: pointer; color: var(--text-secondary); border-radius: 6px;">🧩 Perguntas & BNCC</button>
</div>
<!-- ÁREA DO CONTEÚDO ATIVO -->
<div id="historiaDisplayArea" style="display: flex; flex-direction: column; gap: 12px; flex: 1;">
<div id="historiaPlaceholderText" style="text-align: center; padding: 40px 20px; color: var(--text-secondary); background: rgba(255,255,255,0.02); border: 1px dashed var(--border-light); border-radius: 10px;">
<span style="font-size: 2.5rem; display: block; margin-bottom: 10px;">📖</span>
<strong style="color: var(--text-primary); font-size: 0.95rem;">Pronto para inventar uma história inesquecível?</strong>
<p style="font-size: 0.82rem; margin-top: 6px; opacity: 0.8;">Escolha um modelo ou digite o tema ao lado e clique em "Inventar História" para gerar os atos narrativos, ilustrações e áudio narrado.</p>
</div>
<!-- CONTAINER DA HISTÓRIA ATIVA -->
<div id="historiaActiveBox" style="display: none; background: rgba(255,255,255,0.03); border: 1px solid var(--border-light); border-radius: 12px; padding: 16px; flex-direction: column; gap: 12px;">
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px dashed var(--border-light); padding-bottom: 8px; flex-wrap: wrap; gap: 8px;">
<div>
<h4 id="historiaActiveTitle" style="margin: 0; color: #10b981; font-size: 1.05rem; font-family: 'Fredoka', sans-serif;">Título da História</h4>
<span id="historiaActiveSubtitle" style="font-size: 0.72rem; color: var(--text-secondary);">Personagem: João • 3 Cenas</span>
</div>
<div style="display: flex; gap: 6px;">
<button type="button" id="btnCopyActiveHistoria" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 4px 8px; border-radius: 6px; font-size: 0.72rem; cursor: pointer;" title="Copiar história">📋 Copiar</button>
<button type="button" id="btnDownloadActiveHistoriaTxt" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 4px 8px; border-radius: 6px; font-size: 0.72rem; cursor: pointer;" title="Baixar TXT">💾 TXT</button>
<button type="button" id="btnSendHistoriaToMusic" style="background: rgba(168,85,247,0.15); border: 1px solid rgba(168,85,247,0.3); color: #c084fc; padding: 4px 8px; border-radius: 6px; font-size: 0.72rem; cursor: pointer; font-weight: 600;" title="Criar música desta história">🎵 Musicar</button>
<button type="button" id="btnExportActiveHistoriaPdf" style="background: #10b981; color: white; border: none; padding: 4px 10px; border-radius: 6px; font-size: 0.72rem; font-weight: 700; cursor: pointer;" title="Salvar PDF formatado">📄 PDF</button>
</div>
</div>
<!-- VIEW 1: LEITOR INTERATIVO DE CENAS COM ILUSTRAÇÕES -->
<div id="historiaCenasView" style="display: flex; flex-direction: column; gap: 12px;">
<!-- Barra de Navegação entre Cenas -->
<div style="display: flex; justify-content: space-between; align-items: center; background: rgba(0,0,0,0.12); padding: 6px 12px; border-radius: 8px;">
<button type="button" id="btnPrevCena" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-primary); padding: 4px 10px; border-radius: 6px; font-size: 0.75rem; cursor: pointer;">◀ Cena Anterior</button>
<span id="historiaCenaIndicator" style="font-size: 0.8rem; font-weight: 700; color: #34d399;">Cena 1 de 3</span>
<button type="button" id="btnNextCena" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-primary); padding: 4px 10px; border-radius: 6px; font-size: 0.75rem; cursor: pointer;">Próxima Cena ▶</button>
</div>
<!-- Card da Cena Atual -->
<div id="historiaCenaCard" style="display: flex; flex-direction: column; gap: 10px; background: rgba(0,0,0,0.06); padding: 14px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.05);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h5 id="historiaCenaTitle" style="margin: 0; color: #34d399; font-size: 0.95rem;">Ato 1: O Descobrimento</h5>
<button type="button" id="btnGenerateSceneImage" style="background: rgba(59,130,246,0.15); border: 1px solid rgba(59,130,246,0.3); color: #60a5fa; padding: 4px 8px; border-radius: 6px; font-size: 0.72rem; cursor: pointer; font-weight: 600; display: flex; align-items: center; gap: 4px;">
🎨 Gerar Ilustração
</button>
</div>
<!-- Container da Imagem da Cena -->
<div id="historiaCenaImageContainer" style="display: none; width: 100%; border-radius: 8px; overflow: hidden; border: 1px solid var(--border-light); max-height: 200px; text-align: center; background: rgba(0,0,0,0.2);">
<img id="historiaCenaImg" src="" alt="Ilustração da Cena" style="max-height: 200px; width: auto; object-fit: contain;">
</div>
<!-- Texto da Cena -->
<div id="historiaCenaText" style="font-family: 'Fredoka', 'Outfit', sans-serif; font-size: 1.05rem; line-height: 1.7; color: var(--text-primary); white-space: pre-wrap; max-height: 220px; overflow-y: auto;"></div>
</div>
</div>
<!-- VIEW 2: HISTÓRIA COMPLETA -->
<div id="historiaTextoView" style="display: none; font-family: 'Fredoka', 'Outfit', sans-serif; font-size: 1.05rem; line-height: 1.7; color: var(--text-primary); white-space: pre-wrap; background: rgba(0,0,0,0.08); padding: 14px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.05); max-height: 300px; overflow-y: auto;"></div>
<!-- VIEW 3: ATIVIDADES & BNCC -->
<div id="historiaAtividadesView" style="display: none; flex-direction: column; gap: 12px;">
<div style="background: rgba(16,185,129,0.08); border: 1px solid rgba(16,185,129,0.2); border-radius: 8px; padding: 12px; font-size: 0.85rem; color: #a7f3d0; line-height: 1.5;">
<strong>🎯 Objetivos de Aprendizagem & BNCC:</strong><br>
<span id="historiaBnccDesc">EI02EF04 / EI03EF05 • Escuta, Fala, Pensamento e Imaginação</span>
</div>
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border-light); border-radius: 8px; padding: 14px;">
<strong style="color: #34d399; font-size: 0.88rem; display: block; margin-bottom: 8px;">🧩 Perguntas de Compreensão & Roda de Conversa:</strong>
<div id="historiaPerguntasList" style="display: flex; flex-direction: column; gap: 6px; font-size: 0.85rem; color: var(--text-primary); line-height: 1.4;"></div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Modal: Histórico de Histórias -->
<div id="historiaHistoryModal" class="settings-modal" style="display: none; z-index: 10001;"> <div id="historiaHistoryModal" class="settings-modal" style="display: none; z-index: 10001;">
<div class="settings-modal-content" style="max-width: 500px; width: 95%; max-height: 80vh; display: flex; flex-direction: column;"> <div class="settings-modal-content" style="max-width: 540px; 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);"> <div class="settings-modal-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light); padding: 14px 20px; display: flex; justify-content: space-between; align-items: center;">
<h3 style="display: flex; align-items: center; gap: 8px; font-family: 'Outfit', sans-serif;">🕒 Histórico de Histórias</h3> <h3 style="margin: 0; display: flex; align-items: center; gap: 8px; font-family: 'Outfit', sans-serif; font-size: 1.1rem; color: #34d399;">🕒 Histórico de Histórias Geradas</h3>
<button id="btnCloseHistoriaHistory" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem;">&times;</button> <button type="button" id="btnCloseHistoriaHistory" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem; line-height: 1;">&times;</button>
</div> </div>
<div id="historiaHistoryList" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px;"></div> <div id="historiaHistoryList" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; flex: 1;"></div>
</div> </div>
</div> </div>
<!-- Modal: Planejamento Mind Lab --> <!-- Modal: Planejamento Mind Lab -->
+12 -1
View File
@@ -4134,7 +4134,8 @@ code {
@media (max-width: 768px) { @media (max-width: 768px) {
#musicaLeftPanel, #musicaRightPanel, #musicaLeftPanel, #musicaRightPanel,
#poesiaLeftPanel, #poesiaRightPanel { #poesiaLeftPanel, #poesiaRightPanel,
#historiaLeftPanel, #historiaRightPanel {
min-width: 100% !important; min-width: 100% !important;
width: 100% !important; width: 100% !important;
} }
@@ -4150,6 +4151,16 @@ code {
100% { height: 40%; background-color: #fbbf24; } 100% { height: 40%; background-color: #fbbf24; }
} }
.wave-bar-historia.wave-active {
animation: wave-pulse-historia 0.8s infinite ease-in-out alternate;
}
@keyframes wave-pulse-historia {
0% { height: 20%; background-color: #10b981; }
50% { height: 95%; background-color: #34d399; }
100% { height: 40%; background-color: #059669; }
}
+402 -43
View File
@@ -2263,7 +2263,6 @@ app.get('/api/musicas/list', requireAuth, async (req, res) => {
); );
res.json(dbRes.rows); res.json(dbRes.rows);
} catch (err) { } catch (err) {
// Fallback se audio_url não existir ainda na query
try { try {
const dbRes2 = await dbPool.query(`SELECT id, faixa_etaria, ritmo, tema, created_at FROM escola.musicas WHERE usuario_id = $1 ORDER BY created_at DESC`, [usuarioId]); const dbRes2 = await dbPool.query(`SELECT id, faixa_etaria, ritmo, tema, created_at FROM escola.musicas WHERE usuario_id = $1 ORDER BY created_at DESC`, [usuarioId]);
res.json(dbRes2.rows); res.json(dbRes2.rows);
@@ -2292,71 +2291,419 @@ app.delete('/api/musicas/:id', requireAuth, async (req, res) => {
// ============================================================ // ============================================================
// ROTAS DO INVENTANDO HISTÓRIAS // ROTAS DO INVENTANDO HISTÓRIAS (ESTÚDIO PROFISSIONAL)
// ============================================================ // ============================================================
app.post('/api/historias/generate', requireAuth, async (req, res) => { app.post('/api/historias/generate', requireAuth, async (req, res) => {
const { faixaEtaria, paragrafos, personagens, estilo, tema } = req.body; const {
faixaEtaria,
paragrafos,
personagens,
estilo,
tema,
personagemPrincipal,
objetoMagico,
estiloVisual,
turmaId,
gerarAudio
} = req.body;
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000'; const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
if (!faixaEtaria || !paragrafos || !personagens || !estilo || !tema) {
return res.status(400).json({ error: 'Todos os campos são obrigatórios.' }); if (!tema) {
return res.status(400).json({ error: 'O tema da história é obrigatório.' });
} }
try { try {
let idadeInstrucao = ''; const agentConfig = readAgentConfig();
if (faixaEtaria.includes('Bebês') || faixaEtaria.includes('bem pequenas')) { const agentName = agentConfig.agentName || "PedagogIA";
idadeInstrucao = '- NÍVEL FÁCIL: Enredo muito simples, frases curtas, foco em ações diretas.';
} else { const promptHistoria = `Você é a ${agentName}, contadora de histórias e escritora de literatura infantil da professora Camila Martella Gasparini Reifonas.
idadeInstrucao = '- NÍVEL MÉDIO/AVANÇADO: Enredo com lição de moral, imaginação fluida e bom vocabulário.'; A professora Camila deseja inventar uma história pedagógica envolvente para seus alunos na Educação Infantil.
PARÂMETROS DA HISTÓRIA:
- Tema / Enredo: "${tema}"
- Faixa Etária do Público: "${faixaEtaria || 'Crianças pequenas (4 a 5 anos)'}"
- Personagem Principal: "${personagemPrincipal || 'Criança curiosa da turma'}"
- Objeto / Elemento Mágico: "${objetoMagico || 'Arca da Imaginação'}"
- Estilo Narrativo: "${estilo || 'Lúdico, Afetivo e Dialogado'}"
- Estilo Visual das Ilustrações: "${estiloVisual || 'Cartoon colorido e acolhedor'}"
- Quantidade de Cenas / Atos: ${paragrafos || 3}
ESTRUTURA PEDAGÓGICA (3 ATOS):
1. Ato 1 (Introdução): Apresentação do personagem no cotidiano escolar/infantil e descoberta do elemento disparador.
2. Ato 2 (Conflito / Desafio): Uma situação-problema lúdica onde a cooperação, empatia e inteligência emocional são necessárias.
3. Ato 3 (Resolução & Aprendizado): Resolução mágica com celebração da amizade e aprendizado afetivo.
Retorne ESTRITAMENTE um JSON válido (sem tags markdown externas):
{
"titulo": "Título Mágico e Divertido da História",
"personagemPrincipal": "${personagemPrincipal || 'Personagem Principal'}",
"objetoMagico": "${objetoMagico || 'Elemento Especial'}",
"historiaCompleta": "Texto narrativo completo e fluido da história com quebras de parágrafos.",
"cenas": [
{
"numero": 1,
"titulo": "Ato 1: O Descobrimento Mágico",
"texto": "Texto do primeiro ato para contação...",
"promptImagem": "Cute children book illustration of ${personagemPrincipal || 'a happy child'} finding a magical glowing box in a vibrant kindergarten garden, ${estiloVisual || 'cartoon style'}, bright warm colors, highly detailed"
},
{
"numero": 2,
"titulo": "Ato 2: A Grande Descoberta e o Desafio",
"texto": "Texto do segundo ato com a situação-problema...",
"promptImagem": "Children book illustration of friends cooperating together in a classroom, ${estiloVisual || 'cartoon style'}, friendly smiles, soft lighting"
},
{
"numero": 3,
"titulo": "Ato 3: A Festa da Amizade",
"texto": "Texto do terceiro ato com o desfecho feliz...",
"promptImagem": "Children celebration in a colorful school yard with balloons, ${estiloVisual || 'cartoon style'}, joyful atmosphere, happy children"
} }
],
"perguntasCompreensao": [
"1. O que o personagem descobriu no início da história?",
"2. Como os amigos ajudaram a resolver o desafio?",
"3. O que você faria se encontrasse esse objeto mágico?"
],
"bnccCampos": "EI02EF04 / EI03EF05 • Escuta, Fala, Pensamento e Imaginação",
"duracaoLeitura": "3 a 4 minutos",
"complexidade": "Adequada para Educação Infantil (Lúdica e Sonora)"
}`;
const systemInstruction = `Você é um contador de histórias pedagógico focado na educação infantil brasileira. let jsonResponseText = '';
REGRAS:
- Faixa etária: ${faixaEtaria}
${idadeInstrucao}
- Tamanho: Exatamente ${paragrafos} parágrafos.
- Quantidade de personagens: ${personagens}
- Estilo narrativo: ${estilo} (se Dialogada, inclua falas claras entre os personagens; se Narração Direta, apenas o narrador conta tudo).
- Tema: "${tema}"
- Responda APENAS a história em Markdown.`;
let historiaText = ''; // 1. Tentar MiniMax M3
if (process.env.GOOGLE_AI_API_KEY) { try {
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}`; console.log('[Histórias] Criando narrativa com MiniMax M3...');
const response = await fetch(geminiUrl, { const r = await callMinimax({
method: 'POST', system: "Você é uma autora premiada de literatura infantil e contadora de histórias pedagógica no Brasil. Responda estritamente em JSON válido.",
headers: { 'Content-Type': 'application/json' }, messages: [{ role: 'user', content: promptHistoria }],
body: JSON.stringify({ contents: [{ parts: [{ text: systemInstruction }] }] }) temperature: 0.7,
max_tokens: 2500
}); });
if (!response.ok) { jsonResponseText = r.text;
const txt = await response.text(); } catch (err) {
throw new Error(`Gemini Error: ${txt}`); console.error('[Histórias] Erro MiniMax M3:', err.message);
}
const data = await response.json();
historiaText = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
} }
historiaText = historiaText.replace(/<think>[\s\S]*?<\/think>/gi, '').trim(); // 2. Fallback Groq
if (!jsonResponseText && process.env.GROQ_API_KEY) {
try {
console.log('[Histórias] Fallback Groq...');
const r = await fetch('https://api.groq.com/openai/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GROQ_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: process.env.GROQ_MODEL || 'openai/gpt-oss-120b',
messages: [{ role: 'user', content: promptHistoria }],
temperature: 0.7,
max_tokens: 2500,
response_format: { type: 'json_object' }
})
});
if (r.ok) {
const data = await r.json();
jsonResponseText = data.choices?.[0]?.message?.content || '';
}
} catch (err) {
console.error('[Histórias] Erro Groq:', err.message);
}
}
if (!historiaText) throw new Error('Falha ao inventar história (texto vazio)'); // 3. Fallback OpenRouter
if (!jsonResponseText && process.env.OPENROUTER_API_KEY) {
try {
console.log('[Histórias] 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: promptHistoria }],
temperature: 0.7,
max_tokens: 2500
})
});
if (r.ok) {
const data = await r.json();
jsonResponseText = data.choices?.[0]?.message?.content || '';
}
} catch (err) {
console.error('[Histórias] Erro OpenRouter:', err.message);
}
}
const dbRes = await dbPool.query( // 4. Fallback Google Gemini
`INSERT INTO escola.historias (usuario_id, faixa_etaria, paragrafos, personagens, estilo, tema, historia) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, if (!jsonResponseText && process.env.GOOGLE_AI_API_KEY) {
[usuarioId, faixaEtaria, paragrafos, personagens, estilo, tema, historiaText] try {
); console.log('[Histórias] Fallback 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: promptHistoria }] }],
generationConfig: { temperature: 0.7, maxOutputTokens: 2500 }
})
});
if (response.ok) {
const resData = await response.json();
jsonResponseText = resData.candidates?.[0]?.content?.parts?.[0]?.text || '';
}
} catch (err) {
console.error('[Histórias] Erro Gemini:', err.message);
}
}
// Parse JSON
let historiaData = {};
try {
const cleanJson = jsonResponseText.replace(/```json|```/g, '').trim();
historiaData = JSON.parse(cleanJson);
} catch (parseErr) {
console.warn('[Histórias] Erro parse JSON, criando estrutura de contingência:', parseErr.message);
historiaData = {
titulo: `A Aventura de ${personagemPrincipal || 'Nossa Turma'}`,
personagemPrincipal: personagemPrincipal || 'Aluno(a)',
objetoMagico: objetoMagico || 'Elemento Especial',
historiaCompleta: jsonResponseText || `Era uma vez, em um dia cheio de sol na escola, ${personagemPrincipal || 'uma criança'} encontrou um objeto mágico...`,
cenas: [
{ numero: 1, titulo: "Ato 1: O Início da Jornada", texto: "A turma estava reunida quando algo incrível aconteceu...", promptImagem: "Children in kindergarten playing joyfully" },
{ numero: 2, titulo: "Ato 2: O Desafio", texto: "Juntos, eles precisavam cooperar para desvendar o segredo...", promptImagem: "Kids cooperating and sharing" },
{ numero: 3, titulo: "Ato 3: O Final Feliz", texto: "Com muitos abraços e alegria, a história teve o final perfeito!", promptImagem: "Happy children celebrating" }
],
perguntasCompreensao: ["O que aconteceu no início?", "Como eles resolveram o problema?"],
bnccCampos: "EI02EF04 / EI03EF05 • Escuta, Fala, Pensamento e Imaginação",
duracaoLeitura: "3 minutos",
complexidade: "Lúdica (Infantil)"
};
}
// Geração de Áudio Narrado (MiniMax)
let audioUrl = null;
if (gerarAudio !== false && process.env.MINIMAX_API_KEY) {
try {
console.log('[Histórias] Gerando áudio narrado com MiniMax...');
const textToNarrate = (historiaData.historiaCompleta || tema).substring(0, 600);
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
const musicResp = await fetch(`${minmBase}/v1/music_generation`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.MINIMAX_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'music-2.0',
prompt: 'Gentle warm kindergarten teacher storytelling narration, Portuguese Brazilian accent, calm acoustic ambient',
lyrics: textToNarrate,
audio_setting: { sample_rate: 32000, bitrate: 128000, format: 'mp3' }
})
});
if (musicResp.ok) {
const mData = await musicResp.json();
const audioHex = mData.data?.audio;
if (audioHex) {
const fs = require('fs');
const path = require('path');
const mediaDir = path.join(__dirname, 'public', 'generated-media');
if (!fs.existsSync(mediaDir)) {
fs.mkdirSync(mediaDir, { recursive: true });
}
const fileName = `historia_${Date.now()}.mp3`;
const filePath = path.join(mediaDir, fileName);
const audioBuffer = Buffer.from(audioHex, 'hex');
fs.writeFileSync(filePath, audioBuffer);
await backupMediaFile(filePath, audioBuffer);
audioUrl = `/generated-media/${fileName}`;
}
}
} catch (audioErr) {
console.error('[Histórias] Erro ao gerar áudio narrado:', audioErr.message);
}
}
// Salvar no Banco PostgreSQL
let savedId = null;
try {
const dbRes = await dbPool.query(
`INSERT INTO escola.historias (
usuario_id, faixa_etaria, paragrafos, personagens, estilo, tema, historia,
titulo, personagem_principal, objeto_magico, cenas, bncc, turma_id, audio_url,
perguntas_compreensao, duracao_leitura, complexidade, estilo_visual
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) RETURNING id`,
[
usuarioId,
faixaEtaria || 'Crianças pequenas',
parseInt(paragrafos) || 3,
parseInt(personagens) || 2,
estilo || 'Dialogada',
tema,
historiaData.historiaCompleta || '',
historiaData.titulo || `História: ${tema}`,
personagemPrincipal || '',
objetoMagico || '',
JSON.stringify(historiaData.cenas || []),
historiaData.bnccCampos || '',
turmaId || null,
audioUrl,
JSON.stringify(historiaData.perguntasCompreensao || []),
historiaData.duracaoLeitura || '3 min',
historiaData.complexidade || 'Lúdica',
estiloVisual || 'Cartoon colorido'
]
);
savedId = dbRes.rows[0]?.id;
} catch (dbErr) {
console.warn('[Histórias] Erro ao salvar colunas estendidas, salvando básico:', dbErr.message);
try {
const dbRes2 = await dbPool.query(
`INSERT INTO escola.historias (usuario_id, faixa_etaria, paragrafos, personagens, estilo, tema, historia) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
[usuarioId, faixaEtaria || 'Crianças', parseInt(paragrafos) || 3, parseInt(personagens) || 2, estilo || 'Lúdico', tema, historiaData.historiaCompleta || '']
);
savedId = dbRes2.rows[0]?.id;
} catch (err2) {
console.error('[Histórias] Erro ao salvar história:', err2.message);
}
}
res.json({
success: true,
id: savedId,
titulo: historiaData.titulo || `História: ${tema}`,
personagemPrincipal: historiaData.personagemPrincipal || personagemPrincipal,
objetoMagico: historiaData.objetoMagico || objetoMagico,
historiaCompleta: historiaData.historiaCompleta || '',
cenas: historiaData.cenas || [],
perguntasCompreensao: historiaData.perguntasCompreensao || [],
bnccCampos: historiaData.bnccCampos || 'EI02EF04 / EI03EF05 • Escuta, Fala, Pensamento e Imaginação',
duracaoLeitura: historiaData.duracaoLeitura || '3 a 4 min',
complexidade: historiaData.complexidade || 'Lúdica e Sonora',
audioUrl: audioUrl,
tema: tema,
faixaEtaria: faixaEtaria,
estilo: estilo,
estiloVisual: estiloVisual
});
res.json({ id: dbRes.rows[0].id, historia: historiaText });
} catch (err) { } catch (err) {
console.error('Erro História:', err); console.error('Erro ao inventar história:', err);
res.status(500).json({ error: 'Erro ao inventar história: ' + err.message }); res.status(500).json({ error: 'Erro ao gerar história: ' + err.message });
}
});
// Endpoint para gerar ilustração individual de uma cena
app.post('/api/historias/scene-image', requireAuth, async (req, res) => {
const { prompt, estiloVisual, cenaNumero } = req.body;
if (!prompt) {
return res.status(400).json({ error: 'O prompt da cena é obrigatório.' });
}
try {
console.log(`[Histórias] Gerando ilustração para cena ${cenaNumero || 1}...`);
const enhancedPrompt = `${prompt}, ${estiloVisual || 'children book colorful illustration'}, vibrant lighting, cute warm character design, 4k master artwork`;
// 1. Tentar MiniMax image-01
if (process.env.MINIMAX_API_KEY) {
try {
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
const imgResp = 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: enhancedPrompt,
aspect_ratio: '4:3',
response_format: 'base64'
})
});
if (imgResp.ok) {
const imgData = await imgResp.json();
const base64Data = imgData.data?.image_base64 || imgData.data?.images?.[0];
if (base64Data) {
const fs = require('fs');
const path = require('path');
const mediaDir = path.join(__dirname, 'public', 'generated-media');
if (!fs.existsSync(mediaDir)) fs.mkdirSync(mediaDir, { recursive: true });
const fileName = `cena_${Date.now()}.png`;
const filePath = path.join(mediaDir, fileName);
const buffer = Buffer.from(base64Data, 'base64');
fs.writeFileSync(filePath, buffer);
await backupMediaFile(filePath, buffer);
return res.json({ success: true, imageUrl: `/generated-media/${fileName}` });
}
}
} catch (minErr) {
console.warn('[Histórias] Falha MiniMax imagem:', minErr.message);
}
}
// 2. Fallback OpenRouter FLUX/SD
if (process.env.OPENROUTER_API_KEY) {
try {
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: 'black-forest-labs/flux-1-schnell',
messages: [{ role: 'user', content: enhancedPrompt }]
})
});
if (r.ok) {
const data = await r.json();
const imgUrl = data.choices?.[0]?.message?.content;
if (imgUrl && imgUrl.startsWith('http')) {
return res.json({ success: true, imageUrl: imgUrl });
}
}
} catch (orErr) {
console.warn('[Histórias] Falha OpenRouter imagem:', orErr.message);
}
}
// Fallback Imagem Ilustrada Placeholder SVG
res.json({
success: true,
imageUrl: 'https://images.unsplash.com/photo-1512820790803-83ca734da794?w=600&auto=format&fit=crop&q=80'
});
} catch (err) {
console.error('[Histórias] Erro ao gerar imagem da cena:', err);
res.status(500).json({ error: 'Erro ao gerar imagem da cena: ' + err.message });
} }
}); });
app.get('/api/historias/list', requireAuth, async (req, res) => { app.get('/api/historias/list', requireAuth, async (req, res) => {
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000'; const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
try { try {
const dbRes = await dbPool.query(`SELECT id, faixa_etaria, estilo, tema, created_at FROM escola.historias WHERE usuario_id = $1 ORDER BY created_at DESC`, [usuarioId]); const dbRes = await dbPool.query(
`SELECT id, titulo, faixa_etaria, estilo, tema, personagem_principal, audio_url, created_at FROM escola.historias WHERE usuario_id = $1 ORDER BY created_at DESC`,
[usuarioId]
);
res.json(dbRes.rows); res.json(dbRes.rows);
} catch (err) { res.status(500).json({ error: 'Erro ao listar histórico.' }); } } catch (err) {
try {
const dbRes2 = await dbPool.query(
`SELECT id, faixa_etaria, estilo, tema, created_at FROM escola.historias WHERE usuario_id = $1 ORDER BY created_at DESC`,
[usuarioId]
);
res.json(dbRes2.rows);
} catch (err2) {
res.status(500).json({ error: 'Erro ao listar histórico de histórias.' });
}
}
}); });
app.get('/api/historias/:id', requireAuth, async (req, res) => { app.get('/api/historias/:id', requireAuth, async (req, res) => {
@@ -2766,6 +3113,18 @@ async function initDatabase() {
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
); );
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS titulo VARCHAR(255);
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS personagem_principal VARCHAR(255);
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS objeto_magico VARCHAR(255);
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS cenas JSONB;
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS bncc VARCHAR(100);
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS turma_id UUID;
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS audio_url TEXT;
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS perguntas_compreensao JSONB;
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS duracao_leitura VARCHAR(50);
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS complexidade VARCHAR(50);
ALTER TABLE escola.historias ADD COLUMN IF NOT EXISTS estilo_visual VARCHAR(100);
-- Cartazes (Criar Cartazes) -- Cartazes (Criar Cartazes)
CREATE TABLE IF NOT EXISTS escola.cartazes ( CREATE TABLE IF NOT EXISTS escola.cartazes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),