Feature: Fabrica de Quadrinhos profissional com geracao paralela, modo apresentacao fullscreen, storyboard interativo, exportacao ZIP e adaptacao mobile

This commit is contained in:
2026-08-31 11:05:03 +00:00
parent 665b9abe63
commit 36f72f56dc
3 changed files with 686 additions and 96 deletions
+392 -55
View File
@@ -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 = '<span>📦 Baixar Todas as Imagens (ZIP)</span>';
}
});
}
// --- 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 () => {