feat: adiciona recurso de salvar/carregar/excluir projetos na Fábrica de Quadrinhos
This commit is contained in:
+313
-4
@@ -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 = '<option value="">-- Carregar projeto salvo... --</option>';
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -746,6 +746,23 @@
|
||||
</div>
|
||||
<div class="settings-modal-body" style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 18px; flex: 1;">
|
||||
|
||||
<!-- GERENCIADOR DE PROJETOS -->
|
||||
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border-light); padding: 14px; border-radius: 12px; display: flex; flex-direction: column; gap: 10px;">
|
||||
<label style="color: var(--text-secondary); font-size: 0.82rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">📁 Meus Projetos de Quadrinhos</label>
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<select id="comicsProjectSelect" class="obs-select" style="flex: 1; padding: 8px 12px; font-size: 0.9rem;">
|
||||
<option value="">-- Carregar projeto salvo... --</option>
|
||||
</select>
|
||||
<button type="button" id="btnNewComicsProject" class="btn-music-option" style="padding: 8px 14px; font-size: 0.82rem; border-color: var(--text-secondary); color: var(--text-primary); background: transparent; margin: 0; white-space: nowrap;">✨ Novo</button>
|
||||
<button type="button" id="btnDeleteComicsProject" class="btn-music-option" style="padding: 8px 14px; font-size: 0.82rem; border-color: #ef4444; color: #ef4444; background: rgba(239, 68, 68, 0.05); margin: 0; white-space: nowrap; display: none;">🗑️ Excluir</button>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<input type="text" id="comicsProjectTitleInput" class="obs-input" placeholder="Título do Projeto (Ex: Lucas e Mariana no Parque)" style="flex: 1; margin: 0; padding: 8px 12px; font-size: 0.9rem;">
|
||||
<button type="button" id="btnSaveComicsProject" class="btn-music-option" style="padding: 8px 14px; font-size: 0.82rem; border-color: #10a37f; color: white; background: #10a37f; margin: 0; white-space: nowrap; font-weight: 600;">💾 Salvar</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TEMA -->
|
||||
<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. Tema da História</label>
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user