diff --git a/public/app.js b/public/app.js index 86bf212..b30ddd5 100644 --- a/public/app.js +++ b/public/app.js @@ -3483,6 +3483,13 @@ const initApp = () => { const comicsVideoPlayer = document.getElementById('comicsVideoPlayer'); const comicsVideoDownloadBtn = document.getElementById('comicsVideoDownloadBtn'); + const comicsProjectSelect = document.getElementById('comicsProjectSelect'); + const btnNewComicsProject = document.getElementById('btnNewComicsProject'); + const btnDeleteComicsProject = document.getElementById('btnDeleteComicsProject'); + const comicsProjectTitleInput = document.getElementById('comicsProjectTitleInput'); + const btnSaveComicsProject = document.getElementById('btnSaveComicsProject'); + + let currentComicsProjectId = null; let selectedComicsQty = 5; let selectedComicsBubbles = true; let selectedComicsRatio = '16:9'; @@ -3582,23 +3589,325 @@ const initApp = () => { }); } - // Abrir e fechar modal Fábrica de Quadrinhos - const openComicsModal = () => { - fabricaQuadrinhosModal.style.display = 'flex'; + // --- GERENCIADOR DE PROJETOS DE HQ --- + const startNewComicsProject = () => { + currentComicsProjectId = null; + if (comicsProjectSelect) comicsProjectSelect.value = ''; + if (comicsProjectTitleInput) comicsProjectTitleInput.value = ''; + if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'none'; + comicsTemaInput.value = ''; comicsResultArea.style.display = 'none'; comicsGenerateLoader.style.display = 'none'; comicsVideoResult.style.display = 'none'; comicsVideoLoader.style.display = 'none'; btnGenerateComics.disabled = false; - comicsLoaderProgress.style.width = '0%'; + if (comicsLoaderProgress) comicsLoaderProgress.style.width = '0%'; + // Set active state for default quick options + selectedComicsQty = 5; + document.querySelectorAll('.btn-comics-qty').forEach(b => { + if (b.dataset.qty === '5') b.classList.add('active'); + else b.classList.remove('active'); + }); + + selectedComicsBubbles = true; + document.querySelectorAll('.btn-comics-bubbles').forEach(b => { + if (b.dataset.bubbles === 'true') b.classList.add('active'); + else b.classList.remove('active'); + }); + + selectedComicsRatio = '16:9'; + document.querySelectorAll('.btn-comics-ratio').forEach(b => { + if (b.dataset.ratio === '16:9') b.classList.add('active'); + else b.classList.remove('active'); + }); + + if (comicsCenarioSelect) { + comicsCenarioSelect.value = 'no parque de diversões colorido'; + if (comicsCenarioCustomInput) { + comicsCenarioCustomInput.style.display = 'none'; + comicsCenarioCustomInput.value = ''; + } + } + // Inicializar lista de personagens com padrão se estiver vazia comicsCharListContainer.innerHTML = ''; addCharacterRow("Menino", "Lucas", false); addCharacterRow("Menina", "Mariana", false); + + generatedPanelsData = []; + comicsGridContainer.innerHTML = ''; }; + const loadComicsProjectsList = async () => { + if (!comicsProjectSelect) return; + try { + const resp = await fetch('/api/comics/projects'); + if (!resp.ok) throw new Error('Falha ao listar projetos'); + const projects = await resp.json(); + + comicsProjectSelect.innerHTML = ''; + projects.forEach(proj => { + const opt = document.createElement('option'); + opt.value = proj.id; + opt.textContent = `${proj.titulo} (${new Date(proj.updated_at).toLocaleDateString('pt-BR')})`; + comicsProjectSelect.appendChild(opt); + }); + + if (currentComicsProjectId) { + comicsProjectSelect.value = currentComicsProjectId; + } + } catch (err) { + console.error('Erro ao listar projetos:', err); + } + }; + + const loadComicsProject = async (id) => { + if (!id) { + startNewComicsProject(); + return; + } + try { + const resp = await fetch(`/api/comics/projects/${id}`); + if (!resp.ok) throw new Error('Falha ao carregar projeto'); + const { project, panels } = await resp.json(); + + currentComicsProjectId = project.id; + if (comicsProjectTitleInput) comicsProjectTitleInput.value = project.titulo || ''; + comicsTemaInput.value = project.tema || ''; + + if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'block'; + + // Carregar proporção + selectedComicsRatio = project.proporcao || '16:9'; + document.querySelectorAll('.btn-comics-ratio').forEach(b => { + if (b.dataset.ratio === selectedComicsRatio) b.classList.add('active'); + else b.classList.remove('active'); + }); + + // Carregar cenário + if (comicsCenarioSelect) { + const standardOptions = ['no parque de diversões colorido', 'na sala de aula da escola infantil', 'em uma floresta encantada cheia de flores', 'no zoológico interagindo com animais', 'num jardim ensolarado de uma casa']; + if (standardOptions.includes(project.cenario)) { + comicsCenarioSelect.value = project.cenario; + if (comicsCenarioCustomInput) comicsCenarioCustomInput.style.display = 'none'; + } else { + comicsCenarioSelect.value = 'custom'; + if (comicsCenarioCustomInput) { + comicsCenarioCustomInput.style.display = 'block'; + comicsCenarioCustomInput.value = project.cenario || ''; + } + } + } + + // Carregar quantidade e balões baseados nos painéis retornados + selectedComicsQty = panels.length || 5; + document.querySelectorAll('.btn-comics-qty').forEach(b => { + if (parseInt(b.dataset.qty) === selectedComicsQty) b.classList.add('active'); + else b.classList.remove('active'); + }); + + const hasDialogue = panels.some(p => p.dialogue); + selectedComicsBubbles = hasDialogue; + document.querySelectorAll('.btn-comics-bubbles').forEach(b => { + if ((b.dataset.bubbles === 'true') === selectedComicsBubbles) b.classList.add('active'); + else b.classList.remove('active'); + }); + + // Carregar personagens + comicsCharListContainer.innerHTML = ''; + if (project.character_description_global) { + try { + const chars = JSON.parse(project.character_description_global); + if (Array.isArray(chars)) { + chars.forEach(c => { + addCharacterRow(c.type || '', c.name || '', c.isAnimal || false); + }); + } + } catch (e) { + addCharacterRow("Menino", "Lucas", false); + addCharacterRow("Menina", "Mariana", false); + } + } else { + addCharacterRow("Menino", "Lucas", false); + addCharacterRow("Menina", "Mariana", false); + } + + // Carregar os painéis + generatedPanelsData = panels.map(p => ({ + panel_number: p.panel_number, + imageUrl: p.image_url, + dialogue: p.dialogue || '', + image_prompt: p.image_prompt || '' + })); + + if (generatedPanelsData.length > 0) { + comicsResultTitle.textContent = project.titulo || 'História em Quadrinhos'; + comicsGridContainer.innerHTML = ''; + + generatedPanelsData.forEach(panel => { + 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; + img.alt = `Quadro ${panel.panel_number}`; + imgContainer.appendChild(img); + + const badge = document.createElement('div'); + badge.className = 'comic-panel-number-badge'; + badge.textContent = panel.panel_number; + imgContainer.appendChild(badge); + + if (selectedComicsBubbles && panel.dialogue) { + const bubble = document.createElement('div'); + bubble.className = 'comic-speech-bubble'; + bubble.textContent = panel.dialogue; + imgContainer.appendChild(bubble); + } + + panelCard.appendChild(imgContainer); + + if (panel.dialogue) { + const caption = document.createElement('div'); + caption.className = 'comic-caption-text'; + caption.textContent = panel.dialogue; + panelCard.appendChild(caption); + } + + comicsGridContainer.appendChild(panelCard); + }); + + comicsResultArea.style.display = 'flex'; + } else { + comicsResultArea.style.display = 'none'; + } + + comicsVideoResult.style.display = 'none'; + comicsVideoLoader.style.display = 'none'; + btnGenerateComics.disabled = false; + + } catch (err) { + console.error(err); + alert('Erro ao carregar projeto: ' + err.message); + } + }; + + const saveComicsProject = async () => { + const titulo = comicsProjectTitleInput ? comicsProjectTitleInput.value.trim() : ''; + if (!titulo) { + alert('Por favor, digite um título para salvar seu projeto.'); + return; + } + + const characters = []; + document.querySelectorAll('.comic-char-row').forEach(row => { + const type = row.querySelector('.char-type-input').value.trim(); + const name = row.querySelector('.char-name-input').value.trim(); + const isAnimal = row.querySelector('span').textContent === '🐾'; + if (type || name) { + characters.push({ type, name, isAnimal }); + } + }); + + let cenario = comicsCenarioSelect.value; + if (cenario === 'custom') { + cenario = comicsCenarioCustomInput.value.trim(); + } + + const body = { + id: currentComicsProjectId, + titulo, + tema: comicsTemaInput.value.trim(), + cenario, + proporcao: selectedComicsRatio, + character_description_global: JSON.stringify(characters), + panels: generatedPanelsData.map(p => ({ + panel_number: p.panel_number, + image_url: p.imageUrl, + image_prompt: p.image_prompt || '', + dialogue: p.dialogue || '' + })) + }; + + try { + const resp = await fetch('/api/comics/projects', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }); + + if (!resp.ok) { + const err = await resp.json(); + throw new Error(err.error || 'Erro ao salvar projeto'); + } + + const data = await resp.json(); + currentComicsProjectId = data.id; + alert('Projeto salvo com sucesso!'); + if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'block'; + await loadComicsProjectsList(); + } catch (err) { + console.error(err); + alert('Erro ao salvar projeto: ' + err.message); + } + }; + + const deleteComicsProject = async () => { + if (!currentComicsProjectId) return; + if (!confirm('Tem certeza de que deseja excluir este projeto permanentemente?')) return; + + try { + const resp = await fetch(`/api/comics/projects/${currentComicsProjectId}`, { + method: 'DELETE' + }); + + if (!resp.ok) throw new Error('Falha ao excluir projeto'); + + alert('Projeto excluído com sucesso!'); + startNewComicsProject(); + await loadComicsProjectsList(); + } catch (err) { + console.error(err); + alert('Erro ao excluir projeto: ' + err.message); + } + }; + + // Abrir e fechar modal Fábrica de Quadrinhos + const openComicsModal = () => { + fabricaQuadrinhosModal.style.display = 'flex'; + startNewComicsProject(); + loadComicsProjectsList(); + }; + + // Listeners do Gerenciador de Projetos + if (comicsProjectSelect) { + comicsProjectSelect.addEventListener('change', (e) => { + loadComicsProject(e.target.value); + }); + } + + if (btnNewComicsProject) { + btnNewComicsProject.addEventListener('click', () => { + if (confirm('Deseja iniciar um novo projeto? Alterações não salvas serão perdidas.')) { + startNewComicsProject(); + } + }); + } + + if (btnSaveComicsProject) { + btnSaveComicsProject.addEventListener('click', saveComicsProject); + } + + if (btnDeleteComicsProject) { + btnDeleteComicsProject.addEventListener('click', deleteComicsProject); + } + if (barBtnComics) { barBtnComics.addEventListener('click', openComicsModal); } diff --git a/public/index.html b/public/index.html index 57b17a4..4a646e5 100644 --- a/public/index.html +++ b/public/index.html @@ -746,6 +746,23 @@
+ +
+ +
+ + + +
+ +
+ + +
+
+
diff --git a/server.js b/server.js index 10e4839..d4a2084 100644 --- a/server.js +++ b/server.js @@ -561,6 +561,136 @@ app.post('/api/comics/generate-video', requireAuth, async (req, res) => { } }); +// --- ROTAS DE PROJETOS DE HQ (SALVAR, LISTAR, CARREGAR, EXCLUIR) --- + +// 1. Listar projetos +app.get('/api/comics/projects', requireAuth, async (req, res) => { + const usuarioId = req.userId || '00000000-0000-0000-0000-000000000000'; + try { + const result = await dbPool.query( + 'SELECT id, titulo, tema, cenario, proporcao, character_description_global, created_at, updated_at FROM escola.projetos_comics WHERE usuario_id = $1 ORDER BY updated_at DESC;', + [usuarioId] + ); + res.json(result.rows); + } catch (error) { + console.error('Erro ao listar projetos de quadrinhos:', error); + res.status(500).json({ error: 'Erro ao listar projetos de quadrinhos.' }); + } +}); + +// 2. Carregar projeto específico +app.get('/api/comics/projects/:id', requireAuth, async (req, res) => { + const usuarioId = req.userId || '00000000-0000-0000-0000-000000000000'; + const { id } = req.params; + try { + const projectRes = await dbPool.query( + 'SELECT * FROM escola.projetos_comics WHERE id = $1 AND usuario_id = $2;', + [id, usuarioId] + ); + if (projectRes.rows.length === 0) { + return res.status(404).json({ error: 'Projeto não encontrado.' }); + } + + const panelsRes = await dbPool.query( + 'SELECT id, panel_number, image_url, image_prompt, dialogue FROM escola.comics_panels WHERE projeto_id = $1 ORDER BY panel_number ASC;', + [id] + ); + + res.json({ + project: projectRes.rows[0], + panels: panelsRes.rows + }); + } catch (error) { + console.error('Erro ao obter projeto de quadrinhos:', error); + res.status(500).json({ error: 'Erro ao obter projeto de quadrinhos.' }); + } +}); + +// 3. Salvar / Atualizar projeto +app.post('/api/comics/projects', requireAuth, async (req, res) => { + const usuarioId = req.userId || '00000000-0000-0000-0000-000000000000'; + const { id, titulo, tema, cenario, proporcao, character_description_global, panels } = req.body; + + if (!titulo) { + return res.status(400).json({ error: 'O título do projeto é obrigatório.' }); + } + + const client = await dbPool.connect(); + try { + await client.query('BEGIN'); + + let projetoId = id; + if (projetoId) { + // Verificar propriedade + const check = await client.query( + 'SELECT id FROM escola.projetos_comics WHERE id = $1 AND usuario_id = $2;', + [projetoId, usuarioId] + ); + if (check.rows.length === 0) { + throw new Error('Projeto não encontrado para edição.'); + } + + // Atualizar metadados + await client.query( + `UPDATE escola.projetos_comics + SET titulo = $1, tema = $2, cenario = $3, proporcao = $4, character_description_global = $5, updated_at = NOW() + WHERE id = $6;`, + [titulo, tema, cenario, proporcao, character_description_global, projetoId] + ); + } else { + // Criar novo projeto + const insertProj = await client.query( + `INSERT INTO escola.projetos_comics (usuario_id, titulo, tema, cenario, proporcao, character_description_global) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING id;`, + [usuarioId, titulo, tema, cenario, proporcao, character_description_global] + ); + projetoId = insertProj.rows[0].id; + } + + // Atualizar painéis: remover antigos se existiam e salvar novos + await client.query('DELETE FROM escola.comics_panels WHERE projeto_id = $1;', [projetoId]); + + if (panels && Array.isArray(panels)) { + for (const p of panels) { + await client.query( + `INSERT INTO escola.comics_panels (projeto_id, panel_number, image_url, image_prompt, dialogue) + VALUES ($1, $2, $3, $4, $5);`, + [projetoId, p.panel_number, p.image_url, p.image_prompt, p.dialogue] + ); + } + } + + await client.query('COMMIT'); + res.json({ success: true, id: projetoId, message: 'Projeto salvo com sucesso!' }); + } catch (error) { + await client.query('ROLLBACK'); + console.error('Erro ao salvar projeto de quadrinhos:', error); + res.status(500).json({ error: 'Erro ao salvar projeto: ' + error.message }); + } finally { + client.release(); + } +}); + +// 4. Excluir projeto +app.delete('/api/comics/projects/:id', requireAuth, async (req, res) => { + const usuarioId = req.userId || '00000000-0000-0000-0000-000000000000'; + const { id } = req.params; + try { + const result = await dbPool.query( + 'DELETE FROM escola.projetos_comics WHERE id = $1 AND usuario_id = $2 RETURNING id;', + [id, usuarioId] + ); + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Projeto não encontrado ou sem permissão para exclusão.' }); + } + res.json({ success: true, message: 'Projeto excluído com sucesso!' }); + } catch (error) { + console.error('Erro ao excluir projeto de quadrinhos:', error); + res.status(500).json({ error: 'Erro ao excluir projeto de quadrinhos.' }); + } +}); + // Rota GET para obter configurações do agente app.get('/api/agent-config', requireAuth, (req, res) => { const config = readAgentConfig();