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:
+527
-65
@@ -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 historiaModal = document.getElementById('historiaModal');
|
||||
const btnCloseHistoriaModal = document.getElementById('btnCloseHistoriaModal');
|
||||
@@ -7703,35 +7762,118 @@ window.toggleCustomAudio = (btn) => {
|
||||
const historiaHistoryModal = document.getElementById('historiaHistoryModal');
|
||||
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 historiaParagrafos = document.getElementById('historiaParagrafos');
|
||||
const historiaPersonagens = document.getElementById('historiaPersonagens');
|
||||
const historiaEstilo = document.getElementById('historiaEstilo');
|
||||
const historiaEstiloVisual = document.getElementById('historiaEstiloVisual');
|
||||
const historiaTemaInput = document.getElementById('historiaTemaInput');
|
||||
const btnGenerateHistoria = document.getElementById('btnGenerateHistoria');
|
||||
|
||||
const historiaGenerateLoader = document.getElementById('historiaGenerateLoader');
|
||||
const historiaResultArea = document.getElementById('historiaResultArea');
|
||||
const historiaTextOutput = document.getElementById('historiaTextOutput');
|
||||
const btnCopyHistoria = document.getElementById('btnCopyHistoria');
|
||||
const btnDownloadHistoriaPdf = document.getElementById('btnDownloadHistoriaPdf');
|
||||
|
||||
let currentHistoria = '';
|
||||
// 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) {
|
||||
barBtnHistoria.addEventListener('click', () => {
|
||||
historiaModal.style.display = 'flex';
|
||||
historiaTemaInput.value = '';
|
||||
historiaResultArea.style.display = 'none';
|
||||
historiaGenerateLoader.style.display = 'none';
|
||||
btnGenerateHistoria.disabled = false;
|
||||
currentHistoria = '';
|
||||
// Player de Áudio
|
||||
const historiaAudioPlayerBox = document.getElementById('historiaAudioPlayerBox');
|
||||
const historiaAudioPlayer = document.getElementById('historiaAudioPlayer');
|
||||
const btnDownloadHistoriaAudio = document.getElementById('btnDownloadHistoriaAudio');
|
||||
const historiaWaveBars = document.querySelectorAll('.wave-bar-historia');
|
||||
|
||||
// Abas
|
||||
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'; });
|
||||
if (btnCloseHistoriaModal) btnCloseHistoriaModal.addEventListener('click', () => historiaModal.style.display = 'none');
|
||||
// Abertura do Modal
|
||||
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) {
|
||||
btnOpenHistoriaHistory.addEventListener('click', () => {
|
||||
historiaHistoryModal.style.display = 'flex';
|
||||
@@ -7741,91 +7883,356 @@ window.toggleCustomAudio = (btn) => {
|
||||
if (btnCloseHistoriaHistory) btnCloseHistoriaHistory.addEventListener('click', () => 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) {
|
||||
btnGenerateHistoria.addEventListener('click', async () => {
|
||||
const tema = historiaTemaInput.value.trim();
|
||||
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;
|
||||
}
|
||||
|
||||
btnGenerateHistoria.disabled = true;
|
||||
historiaResultArea.style.display = 'none';
|
||||
historiaGenerateLoader.style.display = 'flex';
|
||||
if (historiaGenerateLoader) historiaGenerateLoader.style.display = 'flex';
|
||||
if (historiaPlaceholderText) historiaPlaceholderText.style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/historias/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
faixaEtaria: historiaFaixaEtaria.value,
|
||||
paragrafos: historiaParagrafos.value,
|
||||
personagens: historiaPersonagens.value,
|
||||
estilo: historiaEstilo.value,
|
||||
tema: tema
|
||||
tema: tema,
|
||||
faixaEtaria: historiaFaixaEtaria ? historiaFaixaEtaria.value : 'Crianças pequenas',
|
||||
paragrafos: historiaParagrafos ? historiaParagrafos.value : '3',
|
||||
estilo: historiaEstilo ? historiaEstilo.value : 'Lúdico',
|
||||
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();
|
||||
|
||||
if (response.ok) {
|
||||
currentHistoria = data.historia;
|
||||
historiaTextOutput.innerHTML = window.marked ? marked.parse(currentHistoria) : currentHistoria;
|
||||
historiaResultArea.style.display = 'flex';
|
||||
if (response.ok && data.success) {
|
||||
currentHistoriaData = data;
|
||||
currentSceneIndex = 0;
|
||||
|
||||
// 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 {
|
||||
showToast(data.error || 'Erro ao inventar história.', 'error');
|
||||
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 {
|
||||
showToast(data.error || 'Erro ao gerar história.', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast('Erro de conexão.', 'error');
|
||||
showToast('Erro de conexão ao gerar história.', 'error');
|
||||
} finally {
|
||||
historiaGenerateLoader.style.display = 'none';
|
||||
if (historiaGenerateLoader) historiaGenerateLoader.style.display = 'none';
|
||||
btnGenerateHistoria.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (btnCopyHistoria) {
|
||||
btnCopyHistoria.addEventListener('click', () => {
|
||||
if (currentHistoria) {
|
||||
navigator.clipboard.writeText(currentHistoria);
|
||||
// Copiar História
|
||||
if (btnCopyActiveHistoria) {
|
||||
btnCopyActiveHistoria.addEventListener('click', () => {
|
||||
if (currentHistoriaData && currentHistoriaData.historiaCompleta) {
|
||||
navigator.clipboard.writeText(currentHistoriaData.historiaCompleta);
|
||||
showToast('História copiada para a área de transferência!', 'success');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (btnDownloadHistoriaPdf) {
|
||||
btnDownloadHistoriaPdf.addEventListener('click', () => {
|
||||
if (!currentHistoria) return;
|
||||
const printWindow = window.open('', '_blank');
|
||||
printWindow.document.write(`
|
||||
<html>
|
||||
// Baixar TXT
|
||||
if (btnDownloadActiveHistoriaTxt) {
|
||||
btnDownloadActiveHistoriaTxt.addEventListener('click', () => {
|
||||
if (!currentHistoriaData) return;
|
||||
const content = `=========================================
|
||||
📖 ${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>
|
||||
<title>História - Assistente PedaGog</title>
|
||||
<meta charset="UTF-8">
|
||||
<title>${currentHistoriaData.titulo || 'História Pedagógica'} - Pedagog</title>
|
||||
<style>
|
||||
body { font-family: 'Outfit', 'Inter', sans-serif; padding: 40px; color: #333; line-height: 1.6; }
|
||||
h1, h2, h3 { color: #10b981; margin-top: 24px; margin-bottom: 12px; }
|
||||
@media print { body { padding: 0; } button { display: none; } }
|
||||
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 { color: #059669; margin: 0 0 6px 0; font-size: 1.8rem; }
|
||||
.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>
|
||||
</head>
|
||||
<body>
|
||||
<div style="max-width: 800px; margin: 0 auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #e2e8f0; padding-bottom: 10px; margin-bottom: 20px;">
|
||||
<h1 style="margin: 0;">✍️ História Pedagógica</h1>
|
||||
<button onclick="window.print()" style="background: #10b981; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: bold;">Imprimir / PDF</button>
|
||||
<button class="btn-print" onclick="window.print()">🖨️ Imprimir / Salvar PDF</button>
|
||||
<div class="header-box">
|
||||
<h1>📖 ${currentHistoriaData.titulo || 'História Pedagógica'}</h1>
|
||||
<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>${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>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
printWindow.document.close();
|
||||
printWin.document.close();
|
||||
});
|
||||
}
|
||||
|
||||
// Carregar Histórico
|
||||
async function loadHistoriaHistory() {
|
||||
const listContainer = document.getElementById('historiaHistoryList');
|
||||
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 {
|
||||
const response = await fetch('/api/historias/list');
|
||||
const data = await response.json();
|
||||
@@ -7835,19 +8242,24 @@ window.toggleCustomAudio = (btn) => {
|
||||
const date = new Date(item.created_at).toLocaleString('pt-BR');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'history-item-card';
|
||||
div.style.cssText = 'background: rgba(255,255,255,0.02); padding: 14px; border-radius: 8px; border: 1px solid var(--border-light); display: flex; flex-direction: column; gap: 8px; cursor: pointer; position: relative;';
|
||||
div.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 style="display: flex; justify-content: space-between; align-items: flex-start; padding-right: 30px;">
|
||||
<strong style="color: var(--brand-green); font-size: 0.95rem;">${item.faixa_etaria} (${item.estilo})</strong>
|
||||
<span style="font-size: 0.75rem; color: var(--text-secondary);">${date}</span>
|
||||
<strong style="color: #34d399; font-size: 0.95rem;">${item.titulo || item.tema || 'História'}</strong>
|
||||
<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>
|
||||
<p style="margin: 0; font-size: 0.85rem; color: var(--text-primary); opacity: 0.9;">Tema: ${item.tema}</p>
|
||||
<button class="btn-delete-historia" data-id="${item.id}" style="position: absolute; top: 12px; right: 12px; background: transparent; border: none; color: #ef4444; cursor: pointer; font-size: 1.1rem; padding: 4px; border-radius: 4px;" title="Excluir">🗑️</button>
|
||||
`;
|
||||
div.addEventListener('click', async (e) => {
|
||||
if (e.target.closest('.btn-delete-historia')) {
|
||||
e.stopPropagation();
|
||||
if (confirm('Tem certeza que deseja excluir?')) {
|
||||
if (confirm('Tem certeza que deseja excluir esta história?')) {
|
||||
try {
|
||||
const delRes = await fetch(`/api/historias/${item.id}`, { method: 'DELETE' });
|
||||
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 detail = await res.json();
|
||||
if (res.ok) {
|
||||
currentHistoria = detail.historia;
|
||||
historiaTextOutput.innerHTML = window.marked ? marked.parse(currentHistoria) : currentHistoria;
|
||||
historiaResultArea.style.display = 'flex';
|
||||
currentHistoriaData = {
|
||||
id: detail.id,
|
||||
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';
|
||||
}
|
||||
} catch (err) { showToast('Erro ao carregar.', 'error'); }
|
||||
} catch (err) { showToast('Erro ao carregar história.', 'error'); }
|
||||
});
|
||||
listContainer.appendChild(div);
|
||||
});
|
||||
} 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 histórico de histórias.</p>';
|
||||
}
|
||||
} catch (err) { listContainer.innerHTML = '<p style="text-align:center; color:#ef4444;">Erro ao carregar.</p>'; }
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
|
||||
+245
-59
@@ -1208,88 +1208,274 @@
|
||||
</div>
|
||||
|
||||
<!-- ========================================== -->
|
||||
<!-- MODAL: INVENTANDO HISTORIAS -->
|
||||
<!-- MODAL: INVENTANDO HISTÓRIAS (ESTÚDIO PROFISSIONAL) -->
|
||||
<!-- ========================================== -->
|
||||
<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-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;">×</button>
|
||||
<div class="settings-modal-content" style="max-width: 1080px; width: 95%; max-height: 92vh; display: flex; flex-direction: column;">
|
||||
|
||||
<!-- Header -->
|
||||
<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 style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 1.4rem;">📖</span>
|
||||
<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>
|
||||
<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>
|
||||
</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;">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 12px 20px; display: flex; justify-content: flex-end; border-bottom: 1px solid var(--border-light);">
|
||||
<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>
|
||||
<!-- Body em 2 Colunas -->
|
||||
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: row; gap: 24px; flex: 1; flex-wrap: wrap;">
|
||||
|
||||
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 18px; flex: 1;">
|
||||
<!-- Lado Esquerdo: Criação & Parâmetros Narrativos -->
|
||||
<div id="historiaLeftPanel" style="flex: 1; min-width: 320px; display: flex; flex-direction: column; gap: 14px;">
|
||||
|
||||
<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>
|
||||
|
||||
<!-- TEMPLATES RÁPIDOS DE HISTÓ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;">1. Faixa Etária</label>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- TURMA & PERSONAGEM PRINCIPAL -->
|
||||
<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>
|
||||
|
||||
<!-- OBJETO MÁGICO / ELEMENTO DISPARADOR -->
|
||||
<div class="settings-group">
|
||||
<label style="color: var(--text-secondary); font-size: 0.75rem; font-weight: 600; text-transform: uppercase;">Objeto Mágico ou Elemento Disparador</label>
|
||||
<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;">
|
||||
</div>
|
||||
|
||||
<!-- TEMA / ENREDO -->
|
||||
<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>
|
||||
|
||||
<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 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>
|
||||
<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 style="display: flex; gap: 12px; align-items: flex-start;">
|
||||
<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;">Tamanho (Parágrafos)</label>
|
||||
<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="2">2 Parágrafos</option>
|
||||
<option value="3" selected>3 Parágrafos</option>
|
||||
<option value="5">5 Parágrafos</option>
|
||||
<option value="7">7 Parágrafos</option>
|
||||
<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 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>
|
||||
<input type="number" id="historiaPersonagens" value="2" min="1" max="10" class="obs-input" style="width: 100%; margin-top: 4px; padding: 10px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">3. Estilo da Narrativa</label>
|
||||
<!-- 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="Narração Direta">Narração Direta (Apenas Narrador)</option>
|
||||
<option value="Dialogada">Dialogada (Com fala dos personagens)</option>
|
||||
<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">
|
||||
<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 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>
|
||||
<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>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Modal: Histórico de Histórias -->
|
||||
<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-header" style="background: var(--bg-secondary); border-bottom: 1px solid var(--border-light);">
|
||||
<h3 style="display: flex; align-items: center; gap: 8px; font-family: 'Outfit', sans-serif;">🕒 Histórico de Histórias</h3>
|
||||
<button id="btnCloseHistoriaHistory" class="btn-close-modal" style="background: none; border: none; color: var(--text-secondary); cursor: pointer; font-size: 1.5rem;">×</button>
|
||||
<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); padding: 14px 20px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<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 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;">×</button>
|
||||
</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>
|
||||
<!-- Modal: Planejamento Mind Lab -->
|
||||
|
||||
+12
-1
@@ -4134,7 +4134,8 @@ code {
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#musicaLeftPanel, #musicaRightPanel,
|
||||
#poesiaLeftPanel, #poesiaRightPanel {
|
||||
#poesiaLeftPanel, #poesiaRightPanel,
|
||||
#historiaLeftPanel, #historiaRightPanel {
|
||||
min-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
@@ -4150,6 +4151,16 @@ code {
|
||||
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; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2263,7 +2263,6 @@ app.get('/api/musicas/list', requireAuth, async (req, res) => {
|
||||
);
|
||||
res.json(dbRes.rows);
|
||||
} catch (err) {
|
||||
// Fallback se audio_url não existir ainda na query
|
||||
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]);
|
||||
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) => {
|
||||
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';
|
||||
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 {
|
||||
let idadeInstrucao = '';
|
||||
if (faixaEtaria.includes('Bebês') || faixaEtaria.includes('bem pequenas')) {
|
||||
idadeInstrucao = '- NÍVEL FÁCIL: Enredo muito simples, frases curtas, foco em ações diretas.';
|
||||
} else {
|
||||
idadeInstrucao = '- NÍVEL MÉDIO/AVANÇADO: Enredo com lição de moral, imaginação fluida e bom vocabulário.';
|
||||
const agentConfig = readAgentConfig();
|
||||
const agentName = agentConfig.agentName || "PedagogIA";
|
||||
|
||||
const promptHistoria = `Você é a ${agentName}, contadora de histórias e escritora de literatura infantil da professora Camila Martella Gasparini Reifonas.
|
||||
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)"
|
||||
}`;
|
||||
|
||||
let jsonResponseText = '';
|
||||
|
||||
// 1. Tentar MiniMax M3
|
||||
try {
|
||||
console.log('[Histórias] Criando narrativa com MiniMax M3...');
|
||||
const r = await callMinimax({
|
||||
system: "Você é uma autora premiada de literatura infantil e contadora de histórias pedagógica no Brasil. Responda estritamente em JSON válido.",
|
||||
messages: [{ role: 'user', content: promptHistoria }],
|
||||
temperature: 0.7,
|
||||
max_tokens: 2500
|
||||
});
|
||||
jsonResponseText = r.text;
|
||||
} catch (err) {
|
||||
console.error('[Histórias] Erro MiniMax M3:', err.message);
|
||||
}
|
||||
|
||||
const systemInstruction = `Você é um contador de histórias pedagógico focado na educação infantil brasileira.
|
||||
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.`;
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
let historiaText = '';
|
||||
if (process.env.GOOGLE_AI_API_KEY) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fallback Google Gemini
|
||||
if (!jsonResponseText && process.env.GOOGLE_AI_API_KEY) {
|
||||
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: systemInstruction }] }] })
|
||||
body: JSON.stringify({
|
||||
contents: [{ parts: [{ text: promptHistoria }] }],
|
||||
generationConfig: { temperature: 0.7, maxOutputTokens: 2500 }
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
const txt = await response.text();
|
||||
throw new Error(`Gemini Error: ${txt}`);
|
||||
if (response.ok) {
|
||||
const resData = await response.json();
|
||||
jsonResponseText = resData.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
||||
}
|
||||
const data = await response.json();
|
||||
historiaText = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
||||
}
|
||||
|
||||
historiaText = historiaText.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
|
||||
|
||||
if (!historiaText) throw new Error('Falha ao inventar história (texto vazio)');
|
||||
|
||||
const dbRes = 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, paragrafos, personagens, estilo, tema, historiaText]
|
||||
);
|
||||
|
||||
res.json({ id: dbRes.rows[0].id, historia: historiaText });
|
||||
} catch (err) {
|
||||
console.error('Erro História:', err);
|
||||
res.status(500).json({ error: 'Erro ao inventar história: ' + err.message });
|
||||
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
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('Erro ao inventar história:', err);
|
||||
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) => {
|
||||
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
|
||||
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);
|
||||
} 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) => {
|
||||
@@ -2766,6 +3113,18 @@ async function initDatabase() {
|
||||
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)
|
||||
CREATE TABLE IF NOT EXISTS escola.cartazes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
Reference in New Issue
Block a user