From 36f72f56dc4b6e374f8ab9393b70139b2329d507 Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Mon, 31 Aug 2026 11:05:03 +0000 Subject: [PATCH] Feature: Fabrica de Quadrinhos profissional com geracao paralela, modo apresentacao fullscreen, storyboard interativo, exportacao ZIP e adaptacao mobile --- public/app.js | 447 ++++++++++++++++++++++++++++++++++++++++------ public/index.html | 95 +++++++--- public/style.css | 240 ++++++++++++++++++++++--- 3 files changed, 686 insertions(+), 96 deletions(-) diff --git a/public/app.js b/public/app.js index 21c65e7..530a173 100644 --- a/public/app.js +++ b/public/app.js @@ -4442,6 +4442,8 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` comicsCharListContainer.appendChild(row); }; + const btnImportTurmaChars = document.getElementById('btnImportTurmaChars'); + if (btnAddHumanChar) { btnAddHumanChar.addEventListener('click', () => { addCharacterRow('', '', false); @@ -4454,6 +4456,37 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` }); } + if (btnImportTurmaChars) { + btnImportTurmaChars.addEventListener('click', async () => { + try { + btnImportTurmaChars.disabled = true; + btnImportTurmaChars.textContent = '⏳ Carregando...'; + const res = await fetch('/api/alunos'); + if (!res.ok) throw new Error('Falha ao obter lista de alunos'); + const alunos = await res.json(); + if (!alunos || alunos.length === 0) { + await showCustomAlert('Nenhum Aluno', 'Cadastre alunos na seção "Minha Turma" para importá-los automaticamente.'); + return; + } + + // Adiciona os alunos como personagens (até 4 para manter a história focada) + const selecionados = alunos.slice(0, 4); + comicsCharListContainer.innerHTML = ''; + selecionados.forEach(aluno => { + const generoProvavel = aluno.nome.trim().endsWith('a') ? 'Menina' : 'Menino'; + addCharacterRow(generoProvavel, aluno.nome.trim(), false); + }); + await showCustomAlert('Turma Importada!', `${selecionados.length} alunos foram adicionados como personagens da história.`); + } catch (err) { + console.error('Erro ao importar alunos:', err); + await showCustomAlert('Erro', 'Não foi possível carregar a lista de alunos: ' + err.message); + } finally { + btnImportTurmaChars.disabled = false; + btnImportTurmaChars.textContent = '+ 🏫 Importar da Turma'; + } + }); + } + // Exibir/ocultar cenário personalizado if (comicsCenarioSelect) { comicsCenarioSelect.addEventListener('change', () => { @@ -4945,18 +4978,14 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` currentComicsCharDescEnglish = scriptData.character_description; } - // 2. Gerar cada quadrinho sequencialmente + // 2. Gerar quadrinhos em PARALELO com tracking de progresso em tempo real if (genMode !== 'extend') { generatedPanelsData = []; } const totalPanels = scriptData.panels.length; + let completedCount = 0; - for (let i = 0; i < totalPanels; i++) { - const panel = scriptData.panels[i]; - const progressPercent = 15 + Math.floor((i / totalPanels) * 80); - comicsLoaderProgress.style.width = `${progressPercent}%`; - comicsLoaderText.textContent = `Ilustrando quadrinho ${i + 1} de ${totalPanels}...`; - + const panelPromises = scriptData.panels.map(async (panel, idx) => { // Injetamos a proporção e estilo de forma reforçada no prompt const fullPrompt = `${panel.image_prompt}, high resolution children book illustration, cute Pixar style, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`; @@ -4968,65 +4997,43 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` if (!frameResp.ok) { const err = await frameResp.json(); - throw new Error(`Erro no quadrinho ${i + 1}: ${err.error}`); + throw new Error(`Erro no quadrinho ${panel.panel_number}: ${err.error}`); } const frameData = await frameResp.json(); - generatedPanelsData.push({ + completedCount++; + const progressPercent = 15 + Math.floor((completedCount / totalPanels) * 80); + if (comicsLoaderProgress) comicsLoaderProgress.style.width = `${progressPercent}%`; + if (comicsLoaderText) comicsLoaderText.textContent = `⚡ Ilustrando em paralelo: ${completedCount}/${totalPanels} quadros concluídos...`; + + return { panel_number: panel.panel_number, imageUrl: frameData.imageUrl, dialogue: panel.dialogue || '', image_prompt: panel.image_prompt || '' - }); + }; + }); + + const newPanels = await Promise.all(panelPromises); + + // Ordena por panel_number para garantir a sequência correta + newPanels.sort((a, b) => a.panel_number - b.panel_number); + + if (genMode === 'extend') { + generatedPanelsData = generatedPanelsData.concat(newPanels); + } else { + generatedPanelsData = newPanels; } - // Renderizar quadrinhos na tela (todos, incluindo os anteriores caso tenha sido extensão) + // Atualizar cabeçalho e metadados comicsResultTitle.textContent = scriptData.title || comicsResultTitle.textContent || 'Fábrica de Quadrinhos Pedagog'; - comicsGridContainer.innerHTML = ''; + const metaPanels = document.getElementById('comicsMetaPanels'); + if (metaPanels) metaPanels.textContent = `🖼️ ${generatedPanelsData.length} Quadros`; + const metaRatio = document.getElementById('comicsMetaRatio'); + if (metaRatio) metaRatio.textContent = `📐 ${selectedComicsRatio} ${selectedComicsRatio === '16:9' ? 'HD' : 'Retrô'}`; - generatedPanelsData.forEach((panel, index) => { - const panelCard = document.createElement('div'); - panelCard.className = 'comic-panel-card'; - - const imgContainer = document.createElement('div'); - imgContainer.className = `comic-panel-image-container ratio-${selectedComicsRatio.replace(':', '-')}`; - - const img = document.createElement('img'); - img.className = 'comic-panel-image'; - img.src = panel.imageUrl || panel.image_url; - img.alt = `Quadro ${panel.panel_number}`; - imgContainer.appendChild(img); - - // Selo do número - const badge = document.createElement('div'); - badge.className = 'comic-panel-number-badge'; - badge.textContent = panel.panel_number; - imgContainer.appendChild(badge); - - panelCard.appendChild(imgContainer); - - const caption = document.createElement('div'); - caption.className = 'comic-caption-text'; - caption.style.cssText = 'padding: 10px 14px; background: var(--bg-primary); border-top: 1px solid var(--border-light);'; - - const textarea = document.createElement('textarea'); - textarea.className = 'comic-caption-input'; - textarea.value = panel.dialogue || ''; - textarea.placeholder = 'Digite a fala ou legenda do quadrinho...'; - textarea.style.cssText = 'width: 100%; border: 1px solid var(--border-light); background: var(--bg-secondary); color: var(--text-primary); border-radius: 6px; padding: 8px; font-family: inherit; font-size: 0.85rem; resize: vertical; box-sizing: border-box; min-height: 50px;'; - - textarea.addEventListener('input', (e) => { - panel.dialogue = e.target.value; - if (generatedPanelsData[index]) { - generatedPanelsData[index].dialogue = e.target.value; - } - }); - - caption.appendChild(textarea); - panelCard.appendChild(caption); - - comicsGridContainer.appendChild(panelCard); - }); + // Renderizar Storyboard Interativo + renderComicsStoryboard(); // Se for uma nova história derivada de um projeto antigo, limpamos o ID para virar um novo projeto independente ao salvar if (genMode === 'new_story' && currentComicsProjectId) { @@ -5053,6 +5060,336 @@ Se algum dado não for mencionado, deixe a string vazia ou false para boolean.` }); } + // --- RENDERIZADOR DO STORYBOARD PROFISSIONAL DOS QUADRINHOS --- + const renderComicsStoryboard = () => { + if (!comicsGridContainer) return; + comicsGridContainer.innerHTML = ''; + + generatedPanelsData.forEach((panel, index) => { + const panelCard = document.createElement('div'); + panelCard.className = 'comic-panel-card'; + panelCard.dataset.index = index; + + const imgContainer = document.createElement('div'); + imgContainer.className = `comic-panel-image-container ratio-${selectedComicsRatio.replace(':', '-')}`; + + const img = document.createElement('img'); + img.className = 'comic-panel-image'; + img.src = panel.imageUrl || panel.image_url; + img.alt = `Quadro ${panel.panel_number}`; + img.loading = 'lazy'; + imgContainer.appendChild(img); + + // Selo de número do quadrinho + const badge = document.createElement('div'); + badge.className = 'comic-panel-number-badge'; + badge.textContent = panel.panel_number; + imgContainer.appendChild(badge); + + // Barra de Ações Rápidas por Quadro (Mobile & Desktop) + const actionsOverlay = document.createElement('div'); + actionsOverlay.className = 'comic-panel-overlay-actions'; + + // Botão 🔍 Zoom + const btnZoom = document.createElement('button'); + btnZoom.type = 'button'; + btnZoom.className = 'btn-comic-action'; + btnZoom.title = 'Ampliar imagem'; + btnZoom.innerHTML = '🔍'; + btnZoom.addEventListener('click', (e) => { + e.stopPropagation(); + openComicsPresentation(index); + }); + actionsOverlay.appendChild(btnZoom); + + // Botão 🔄 Regenerar Imagem deste Quadro + const btnRegen = document.createElement('button'); + btnRegen.type = 'button'; + btnRegen.className = 'btn-comic-action'; + btnRegen.title = 'Regenerar apenas este quadrinho'; + btnRegen.innerHTML = '🔄'; + btnRegen.addEventListener('click', async (e) => { + e.stopPropagation(); + await regenerateSinglePanel(index, panelCard, img, btnRegen); + }); + actionsOverlay.appendChild(btnRegen); + + // Botão ✏️ Ver/Editar Prompt + const btnPromptToggle = document.createElement('button'); + btnPromptToggle.type = 'button'; + btnPromptToggle.className = 'btn-comic-action'; + btnPromptToggle.title = 'Editar prompt visual'; + btnPromptToggle.innerHTML = '✏️'; + actionsOverlay.appendChild(btnPromptToggle); + + // Botão 💾 Baixar Imagem Individual + const btnDownloadImg = document.createElement('button'); + btnDownloadImg.type = 'button'; + btnDownloadImg.className = 'btn-comic-action'; + btnDownloadImg.title = 'Baixar esta imagem PNG'; + btnDownloadImg.innerHTML = '💾'; + btnDownloadImg.addEventListener('click', (e) => { + e.stopPropagation(); + const a = document.createElement('a'); + a.href = panel.imageUrl || panel.image_url; + a.download = `quadrinho_${panel.panel_number}.png`; + document.body.appendChild(a); + a.click(); + a.remove(); + }); + actionsOverlay.appendChild(btnDownloadImg); + + imgContainer.appendChild(actionsOverlay); + panelCard.appendChild(imgContainer); + + // Área de Legenda / Diálogo + const caption = document.createElement('div'); + caption.className = 'comic-caption-text'; + + const textarea = document.createElement('textarea'); + textarea.className = 'comic-caption-input'; + textarea.value = panel.dialogue || ''; + textarea.placeholder = 'Digite a fala ou legenda do quadrinho...'; + textarea.style.cssText = 'width: 100%; border: 1px solid var(--border-light); background: var(--bg-secondary); color: var(--text-primary); border-radius: 6px; padding: 8px; font-family: inherit; font-size: 0.85rem; resize: vertical; box-sizing: border-box; min-height: 48px;'; + + textarea.addEventListener('input', (e) => { + panel.dialogue = e.target.value; + if (generatedPanelsData[index]) { + generatedPanelsData[index].dialogue = e.target.value; + } + }); + caption.appendChild(textarea); + + // Mini-Editor de Prompt Visual (Oculto por padrão, ativado no botão ✏️) + const promptBox = document.createElement('div'); + promptBox.className = 'comic-prompt-details'; + const promptInput = document.createElement('textarea'); + promptInput.style.cssText = 'width: 100%; font-size: 0.72rem; padding: 4px; background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-light); border-radius: 4px; resize: vertical; min-height: 40px; box-sizing: border-box;'; + promptInput.value = panel.image_prompt || ''; + promptInput.addEventListener('input', (e) => { + panel.image_prompt = e.target.value; + }); + promptBox.appendChild(document.createTextNode('Prompt Visual: ')); + promptBox.appendChild(promptInput); + caption.appendChild(promptBox); + + btnPromptToggle.addEventListener('click', (e) => { + e.stopPropagation(); + promptBox.style.display = promptBox.style.display === 'block' ? 'none' : 'block'; + }); + + panelCard.appendChild(caption); + comicsGridContainer.appendChild(panelCard); + }); + }; + + // Regenerar um único painel + const regenerateSinglePanel = async (index, panelCard, imgElement, btnRegen) => { + const panel = generatedPanelsData[index]; + if (!panel) return; + + try { + btnRegen.disabled = true; + btnRegen.textContent = '⏳'; + imgElement.style.opacity = '0.4'; + + const promptToUse = panel.image_prompt || 'cute children illustration'; + const fullPrompt = `${promptToUse}, high resolution children book illustration, cute Pixar style, ${selectedComicsRatio === '16:9' ? '16:9 aspect ratio' : '4:3 aspect ratio'}`; + + const resp = await fetch('/api/comics/generate-frame', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ prompt: fullPrompt }) + }); + + if (!resp.ok) { + const err = await resp.json(); + throw new Error(err.error || 'Erro na regeneração'); + } + + const data = await resp.json(); + panel.imageUrl = data.imageUrl; + imgElement.src = data.imageUrl + '?v=' + Date.now(); + imgElement.style.opacity = '1'; + } catch (err) { + console.error('Erro ao regenerar painel:', err); + await showCustomAlert('Erro', 'Não foi possível regenerar o quadrinho: ' + err.message); + imgElement.style.opacity = '1'; + } finally { + btnRegen.disabled = false; + btnRegen.innerHTML = '🔄'; + } + }; + + // --- EXPORTAR TODAS AS IMAGENS EM ZIP --- + const btnExportComicsZIP = document.getElementById('btnExportComicsZIP'); + if (btnExportComicsZIP) { + btnExportComicsZIP.addEventListener('click', async () => { + if (!generatedPanelsData || generatedPanelsData.length === 0) { + await showCustomAlert('Aviso', 'Nenhum quadrinho gerado para exportar.'); + return; + } + + try { + btnExportComicsZIP.disabled = true; + btnExportComicsZIP.textContent = '⏳ Criando ZIP...'; + + if (typeof JSZip === 'undefined') { + throw new Error('Biblioteca JSZip não carregada.'); + } + + const zip = new JSZip(); + const folder = zip.folder('quadrinhos'); + const title = (comicsResultTitle?.textContent || 'Historia_Quadrinhos').replace(/[^a-zA-Z0-9_-]/g, '_'); + + let roteiroTxt = `HISTÓRIA EM QUADRINHOS: ${comicsResultTitle?.textContent || 'Fábrica de Quadrinhos'}\n`; + roteiroTxt += `Criado com PedagogIA em ${new Date().toLocaleDateString('pt-BR')}\n\n`; + + for (let i = 0; i < generatedPanelsData.length; i++) { + const panel = generatedPanelsData[i]; + const imgUrl = panel.imageUrl || panel.image_url; + roteiroTxt += `[QUADRO ${panel.panel_number}]\nFala/Legenda: ${panel.dialogue || '(Sem fala)'}\nPrompt Visual: ${panel.image_prompt || ''}\n\n`; + + try { + const resp = await fetch(imgUrl); + const blob = await resp.blob(); + folder.file(`quadrinho_${panel.panel_number}.jpg`, blob); + } catch (fetchErr) { + console.warn(`Erro ao incluir imagem ${panel.panel_number} no ZIP:`, fetchErr); + } + } + + folder.file('roteiro.txt', roteiroTxt); + + const content = await zip.generateAsync({ type: 'blob' }); + const downloadUrl = URL.createObjectURL(content); + const a = document.createElement('a'); + a.href = downloadUrl; + a.download = `${title}_pedagog.zip`; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(downloadUrl); + + } catch (err) { + console.error('Erro ao gerar ZIP:', err); + await showCustomAlert('Erro', 'Não foi possível gerar o ZIP: ' + err.message); + } finally { + btnExportComicsZIP.disabled = false; + btnExportComicsZIP.innerHTML = '📦 Baixar Todas as Imagens (ZIP)'; + } + }); + } + + // --- MODO APRESENTAÇÃO EM TELA CHEIA (LEITURA INFANTIL / MOBILE) --- + const comicsPresModal = document.getElementById('comicsPresentationModal'); + const comicsPresTitle = document.getElementById('comicsPresTitle'); + const comicsPresImage = document.getElementById('comicsPresImage'); + const comicsPresCaption = document.getElementById('comicsPresCaption'); + const comicsPresCounter = document.getElementById('comicsPresCounter'); + const btnPresPrev = document.getElementById('btnPresPrev'); + const btnPresNext = document.getElementById('btnPresNext'); + const btnCloseComicsPresModal = document.getElementById('btnCloseComicsPresModal'); + const btnComicsPresentationMode = document.getElementById('btnComicsPresentationMode'); + + let presCurrentIndex = 0; + + const openComicsPresentation = (index = 0) => { + if (!generatedPanelsData || generatedPanelsData.length === 0) return; + presCurrentIndex = Math.max(0, Math.min(index, generatedPanelsData.length - 1)); + if (comicsPresModal) comicsPresModal.style.display = 'flex'; + updateComicsPresentationView(); + }; + + const closeComicsPresentation = () => { + if (comicsPresModal) comicsPresModal.style.display = 'none'; + }; + + const updateComicsPresentationView = () => { + if (!generatedPanelsData || generatedPanelsData.length === 0) return; + const panel = generatedPanelsData[presCurrentIndex]; + if (!panel) return; + + if (comicsPresTitle) comicsPresTitle.textContent = comicsResultTitle?.textContent || 'Fábrica de Quadrinhos'; + if (comicsPresCounter) comicsPresCounter.textContent = `${presCurrentIndex + 1} / ${generatedPanelsData.length}`; + if (comicsPresImage) comicsPresImage.src = panel.imageUrl || panel.image_url; + if (comicsPresCaption) comicsPresCaption.textContent = panel.dialogue || '(História silenciosa)'; + + if (btnPresPrev) btnPresPrev.disabled = presCurrentIndex === 0; + if (btnPresNext) btnPresNext.disabled = presCurrentIndex === generatedPanelsData.length - 1; + }; + + if (btnComicsPresentationMode) { + btnComicsPresentationMode.addEventListener('click', () => { + openComicsPresentation(0); + }); + } + + if (btnPresPrev) { + btnPresPrev.addEventListener('click', () => { + if (presCurrentIndex > 0) { + presCurrentIndex--; + updateComicsPresentationView(); + } + }); + } + + if (btnPresNext) { + btnPresNext.addEventListener('click', () => { + if (presCurrentIndex < generatedPanelsData.length - 1) { + presCurrentIndex++; + updateComicsPresentationView(); + } + }); + } + + if (btnCloseComicsPresModal) { + btnCloseComicsPresModal.addEventListener('click', closeComicsPresentation); + } + + // Teclado (setas) para o modo apresentação + window.addEventListener('keydown', (e) => { + if (comicsPresModal && comicsPresModal.style.display === 'flex') { + if (e.key === 'ArrowRight' || e.key === 'Space') { + if (presCurrentIndex < generatedPanelsData.length - 1) { + presCurrentIndex++; + updateComicsPresentationView(); + } + } else if (e.key === 'ArrowLeft') { + if (presCurrentIndex > 0) { + presCurrentIndex--; + updateComicsPresentationView(); + } + } else if (e.key === 'Escape') { + closeComicsPresentation(); + } + } + }); + + // Touch Swipe para smartphones no modo apresentação + let touchStartX = 0; + if (comicsPresModal) { + comicsPresModal.addEventListener('touchstart', (e) => { + touchStartX = e.changedTouches[0].screenX; + }, { passive: true }); + + comicsPresModal.addEventListener('touchend', (e) => { + const touchEndX = e.changedTouches[0].screenX; + const diff = touchEndX - touchStartX; + if (Math.abs(diff) > 50) { + if (diff < 0 && presCurrentIndex < generatedPanelsData.length - 1) { + // Swipe para a esquerda -> Próximo + presCurrentIndex++; + updateComicsPresentationView(); + } else if (diff > 0 && presCurrentIndex > 0) { + // Swipe para a direita -> Anterior + presCurrentIndex--; + updateComicsPresentationView(); + } + } + }, { passive: true }); + } + // Exportar PDF Retrato (1 por página) if (btnExportComicsPDFPortrait) { btnExportComicsPDFPortrait.addEventListener('click', async () => { diff --git a/public/index.html b/public/index.html index 68a6dae..6549da5 100644 --- a/public/index.html +++ b/public/index.html @@ -1400,9 +1400,10 @@
-