✨ feat: Estúdio de Cenas em Camadas - IA desmonta imagem em elementos editáveis individualmente no canvas
This commit is contained in:
+192
@@ -7601,6 +7601,198 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let dragStartWidth, dragStartHeight;
|
let dragStartWidth, dragStartHeight;
|
||||||
let activeAssetTab = 'postit';
|
let activeAssetTab = 'postit';
|
||||||
|
|
||||||
|
// --- ESTÚDIO DE CENAS EM CAMADAS ---
|
||||||
|
const btnDecomposeScene = document.getElementById('btnDecomposeScene');
|
||||||
|
const sceneDecomposePrompt = document.getElementById('sceneDecomposePrompt');
|
||||||
|
const sceneDecomposeLoader = document.getElementById('sceneDecomposeLoader');
|
||||||
|
const sceneDecomposeStatus = document.getElementById('sceneDecomposeStatus');
|
||||||
|
const sceneLayersList = document.getElementById('sceneLayersList');
|
||||||
|
const sceneLayersItems = document.getElementById('sceneLayersItems');
|
||||||
|
|
||||||
|
// Cria um item de camada de imagem direto no canvas (background ou sticker)
|
||||||
|
function addSceneLayerToCanvas(layerData) {
|
||||||
|
if (!canvasModeActive || !cartazPrintableArea) return;
|
||||||
|
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'canvas-item canvas-scene-layer';
|
||||||
|
item.setAttribute('data-layer-name', layerData.name || '');
|
||||||
|
item.setAttribute('data-rotation', '0');
|
||||||
|
item.style.position = 'absolute';
|
||||||
|
item.style.left = (layerData.left || 10) + '%';
|
||||||
|
item.style.top = (layerData.top || 10) + '%';
|
||||||
|
item.style.width = (layerData.width || 35) + '%';
|
||||||
|
item.style.height = (layerData.height || 35) + '%';
|
||||||
|
item.style.zIndex = layerData.isBackground ? '1' : String(10 + (layerData.zOffset || 0));
|
||||||
|
item.style.background = 'transparent';
|
||||||
|
|
||||||
|
if (layerData.isBackground) {
|
||||||
|
// Fundo: cobrindo 100% e não selecionável como item de controle
|
||||||
|
item.style.left = '0';
|
||||||
|
item.style.top = '0';
|
||||||
|
item.style.width = '100%';
|
||||||
|
item.style.height = '100%';
|
||||||
|
item.style.zIndex = '0';
|
||||||
|
item.style.pointerEvents = 'none'; // Por padrão, o fundo não captura eventos para facilitar a seleção dos elementos acima
|
||||||
|
item.innerHTML = `<img src="${layerData.url}"
|
||||||
|
style="width:100%; height:100%; object-fit:cover; pointer-events:none; display:block;">`;
|
||||||
|
item.title = '🖼️ Cenário de Fundo';
|
||||||
|
} else {
|
||||||
|
item.innerHTML = `<img src="${layerData.url}"
|
||||||
|
style="width:100%; height:100%; object-fit:contain; pointer-events:none;">`;
|
||||||
|
item.title = '🎬 ' + (layerData.name || 'Elemento');
|
||||||
|
}
|
||||||
|
|
||||||
|
const handle = document.createElement('div');
|
||||||
|
handle.className = 'canvas-resize-handle';
|
||||||
|
item.appendChild(handle);
|
||||||
|
|
||||||
|
cartazPrintableArea.appendChild(item);
|
||||||
|
|
||||||
|
if (!layerData.isBackground) {
|
||||||
|
setupCanvasItemEvents(item);
|
||||||
|
// Permitir que o fundo seja selecionável após clicar nele diretamente
|
||||||
|
} else {
|
||||||
|
// para o fundo, adicionar capacidade de clicar para habilitar arrastar
|
||||||
|
item.style.pointerEvents = 'auto';
|
||||||
|
setupCanvasItemEvents(item);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderiza a lista de camadas no painel lateral
|
||||||
|
function renderSceneLayers(layers) {
|
||||||
|
if (!sceneLayersItems) return;
|
||||||
|
sceneLayersItems.innerHTML = '';
|
||||||
|
|
||||||
|
const allLayers = [{ name: '🖼️ Cenário de Fundo', isBackground: true }, ...layers.map(l => ({ ...l }))];
|
||||||
|
allLayers.forEach((layer, index) => {
|
||||||
|
const chip = document.createElement('div');
|
||||||
|
chip.style.cssText = `display:flex; align-items:center; gap:8px; padding:5px 8px;
|
||||||
|
background:rgba(255,255,255,0.05); border:1px solid rgba(139,92,246,0.2);
|
||||||
|
border-radius:6px; font-size:0.78rem; color:var(--text-primary); cursor:pointer;`;
|
||||||
|
chip.innerHTML = `
|
||||||
|
<span style="font-size:1rem;">${layer.isBackground ? '🌄' : '✨'}</span>
|
||||||
|
<span style="flex:1;">${layer.name}</span>
|
||||||
|
<span style="font-size:0.68rem; color:var(--text-secondary);">Camada ${index}</span>
|
||||||
|
`;
|
||||||
|
chip.title = `${layer.isBackground ? 'Fundo da cena' : 'Elemento: ' + layer.name}`;
|
||||||
|
sceneLayersItems.appendChild(chip);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnDecomposeScene) {
|
||||||
|
btnDecomposeScene.addEventListener('click', async () => {
|
||||||
|
const prompt = sceneDecomposePrompt ? sceneDecomposePrompt.value.trim() : '';
|
||||||
|
if (!prompt) {
|
||||||
|
showToast('Descreva a cena antes de gerar!', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Garantir que o modo canvas está ativo
|
||||||
|
if (!canvasModeActive) {
|
||||||
|
if (cartazEditArea && cartazEditArea.style.display !== 'none') {
|
||||||
|
btnEditCartazToggle && btnEditCartazToggle.click();
|
||||||
|
}
|
||||||
|
enterCanvasMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mostrar loading
|
||||||
|
btnDecomposeScene.disabled = true;
|
||||||
|
btnDecomposeScene.style.opacity = '0.6';
|
||||||
|
if (sceneDecomposeLoader) {
|
||||||
|
sceneDecomposeLoader.style.display = 'flex';
|
||||||
|
}
|
||||||
|
if (sceneLayersList) sceneLayersList.style.display = 'none';
|
||||||
|
|
||||||
|
const statusSteps = [
|
||||||
|
'Planejando a cena com IA...',
|
||||||
|
'Identificando elementos e cenário...',
|
||||||
|
'Gerando imagem de fundo...',
|
||||||
|
'Gerando elementos individuais...',
|
||||||
|
'Montando as camadas no canvas...'
|
||||||
|
];
|
||||||
|
let stepIdx = 0;
|
||||||
|
const statusInterval = setInterval(() => {
|
||||||
|
if (sceneDecomposeStatus && stepIdx < statusSteps.length) {
|
||||||
|
sceneDecomposeStatus.textContent = statusSteps[stepIdx++];
|
||||||
|
}
|
||||||
|
}, 3500);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const titulo = activeCartazData?.titulo || '';
|
||||||
|
const response = await fetch('/api/cartazes/decompose-scene', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ tema: prompt, titulo })
|
||||||
|
});
|
||||||
|
|
||||||
|
clearInterval(statusInterval);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errData.error || 'Erro na geração da cena.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success) throw new Error('Resposta inválida do servidor.');
|
||||||
|
|
||||||
|
if (sceneDecomposeStatus) sceneDecomposeStatus.textContent = 'Montando as camadas no canvas...';
|
||||||
|
|
||||||
|
// Limpar canvas antes de montar (remover outros items de cena se houver)
|
||||||
|
const oldLayers = cartazPrintableArea.querySelectorAll('.canvas-scene-layer');
|
||||||
|
oldLayers.forEach(l => l.remove());
|
||||||
|
|
||||||
|
// Adicionar background
|
||||||
|
if (data.backgroundUrl) {
|
||||||
|
addSceneLayerToCanvas({ url: data.backgroundUrl, name: 'Cenário de Fundo', isBackground: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adicionar cada elemento com posição escalonada
|
||||||
|
if (Array.isArray(data.layers)) {
|
||||||
|
data.layers.forEach((layer, idx) => {
|
||||||
|
if (layer.url) {
|
||||||
|
const spread = [
|
||||||
|
{ left: 10, top: 30 },
|
||||||
|
{ left: 38, top: 35 },
|
||||||
|
{ left: 62, top: 28 },
|
||||||
|
{ left: 25, top: 55 }
|
||||||
|
];
|
||||||
|
const pos = spread[idx % spread.length];
|
||||||
|
addSceneLayerToCanvas({
|
||||||
|
url: layer.url,
|
||||||
|
name: layer.name || `Elemento ${idx + 1}`,
|
||||||
|
isBackground: false,
|
||||||
|
left: pos.left,
|
||||||
|
top: pos.top,
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
zOffset: idx + 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Renderizar lista de camadas no painel
|
||||||
|
renderSceneLayers(data.layers);
|
||||||
|
if (sceneLayersList) sceneLayersList.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast('🎬 Cena montada em camadas! Clique nos elementos para mover, girar e redimensionar.', 'success');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
clearInterval(statusInterval);
|
||||||
|
console.error('[Decompose Scene]', err);
|
||||||
|
showToast('Erro ao criar a cena em camadas: ' + err.message, 'error');
|
||||||
|
} finally {
|
||||||
|
btnDecomposeScene.disabled = false;
|
||||||
|
btnDecomposeScene.style.opacity = '1';
|
||||||
|
if (sceneDecomposeLoader) sceneDecomposeLoader.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function enterCanvasMode() {
|
function enterCanvasMode() {
|
||||||
canvasModeActive = true;
|
canvasModeActive = true;
|
||||||
if (btnCanvasModeToggle) {
|
if (btnCanvasModeToggle) {
|
||||||
|
|||||||
+34
-5
@@ -1330,7 +1330,7 @@
|
|||||||
<!-- GERENCIADOR DE PROJETOS -->
|
<!-- 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;">
|
<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>
|
<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;">
|
<div class="comics-project-row" style="display: flex; gap: 10px; align-items: center;">
|
||||||
<select id="comicsProjectSelect" class="obs-select" style="flex: 1; padding: 8px 12px; font-size: 0.9rem;">
|
<select id="comicsProjectSelect" class="obs-select" style="flex: 1; padding: 8px 12px; font-size: 0.9rem;">
|
||||||
<option value="">-- Carregar projeto salvo... --</option>
|
<option value="">-- Carregar projeto salvo... --</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -1338,7 +1338,7 @@
|
|||||||
<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>
|
<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>
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 10px;">
|
<div class="comics-project-row" 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;">
|
<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; display: none;">💾 Salvar Alterações</button>
|
<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; display: none;">💾 Salvar Alterações</button>
|
||||||
<button type="button" id="btnSaveNewComicsProject" class="btn-music-option" style="padding: 8px 14px; font-size: 0.82rem; border-color: var(--brand-pink); color: white; background: var(--brand-pink); margin: 0; white-space: nowrap; font-weight: 600;">💾 Salvar como Novo</button>
|
<button type="button" id="btnSaveNewComicsProject" class="btn-music-option" style="padding: 8px 14px; font-size: 0.82rem; border-color: var(--brand-pink); color: white; background: var(--brand-pink); margin: 0; white-space: nowrap; font-weight: 600;">💾 Salvar como Novo</button>
|
||||||
@@ -1883,6 +1883,35 @@
|
|||||||
<div id="cartazLeftCanvasPanel" style="display: none; flex: 1; min-width: 320px; flex-direction: column; gap: 16px; padding-right: 12px;">
|
<div id="cartazLeftCanvasPanel" style="display: none; flex: 1; min-width: 320px; flex-direction: column; gap: 16px; padding-right: 12px;">
|
||||||
<h4 style="margin: 0; color: var(--text-primary); border-bottom: 1px dashed var(--border-light); padding-bottom: 8px;">🎨 Elementos do Canvas</h4>
|
<h4 style="margin: 0; color: var(--text-primary); border-bottom: 1px dashed var(--border-light); padding-bottom: 8px;">🎨 Elementos do Canvas</h4>
|
||||||
|
|
||||||
|
<!-- 🎬 ESTÚDIO DE CENAS EM CAMADAS -->
|
||||||
|
<div id="scenesStudioPanel" style="background: linear-gradient(135deg, rgba(139,92,246,0.12), rgba(59,130,246,0.08)); border: 1px solid rgba(139,92,246,0.3); border-radius: 12px; padding: 14px; display: flex; flex-direction: column; gap: 10px;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<span style="font-size: 1.3rem;">🎬</span>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 700; color: var(--text-primary); font-size: 0.9rem;">Estúdio de Cenas em Camadas</div>
|
||||||
|
<div style="font-size: 0.72rem; color: var(--text-secondary); margin-top: 1px;">A IA desmonta a cena em elementos editáveis individualmente</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea id="sceneDecomposePrompt" class="obs-input" rows="3" placeholder="Descreva a cena... Ex: Duas crianças e um cachorrinho num gramado de parque, com um escorregador ao fundo" style="resize: vertical; font-size: 0.85rem; padding: 10px; border-radius: 8px; line-height: 1.4;"></textarea>
|
||||||
|
<button id="btnDecomposeScene" class="btn-save-settings" style="background: linear-gradient(135deg, #8b5cf6, #3b82f6); color: white; border: none; padding: 8px 14px; font-size: 0.82rem; font-weight: 700; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px; transition: opacity 0.2s;">
|
||||||
|
<span>✨ Criar Cena em Camadas</span>
|
||||||
|
</button>
|
||||||
|
<!-- Loading state -->
|
||||||
|
<div id="sceneDecomposeLoader" style="display: none; flex-direction: column; gap: 8px; align-items: center; padding: 10px; background: rgba(0,0,0,0.15); border-radius: 8px;">
|
||||||
|
<div style="display: flex; gap: 6px; align-items: center;">
|
||||||
|
<div class="loading-dot" style="width:8px;height:8px;border-radius:50%;background:#8b5cf6;animation:dot-pulse 1.2s ease-in-out infinite;"></div>
|
||||||
|
<div class="loading-dot" style="width:8px;height:8px;border-radius:50%;background:#8b5cf6;animation:dot-pulse 1.2s ease-in-out 0.4s infinite;"></div>
|
||||||
|
<div class="loading-dot" style="width:8px;height:8px;border-radius:50%;background:#8b5cf6;animation:dot-pulse 1.2s ease-in-out 0.8s infinite;"></div>
|
||||||
|
</div>
|
||||||
|
<div id="sceneDecomposeStatus" style="font-size: 0.78rem; color: var(--text-secondary); text-align: center;">Planejando a cena com IA...</div>
|
||||||
|
</div>
|
||||||
|
<!-- Camadas geradas -->
|
||||||
|
<div id="sceneLayersList" style="display: none; flex-direction: column; gap: 6px;">
|
||||||
|
<div style="font-size: 0.75rem; color: var(--text-secondary); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;">📋 Camadas no Canvas:</div>
|
||||||
|
<div id="sceneLayersItems" style="display: flex; flex-direction: column; gap: 4px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<input type="text" id="canvasAssetSearch" placeholder="🔍 Buscar elementos..." class="obs-input" style="width: 100%; padding: 10px;">
|
<input type="text" id="canvasAssetSearch" placeholder="🔍 Buscar elementos..." class="obs-input" style="width: 100%; padding: 10px;">
|
||||||
</div>
|
</div>
|
||||||
@@ -1901,10 +1930,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Lado Direito: Preview & Editor de Código -->
|
<!-- Lado Direito: Preview & Editor de Código -->
|
||||||
<div style="flex: 1.3; min-width: 360px; display: flex; flex-direction: column; gap: 14px;">
|
<div class="cartaz-preview-panel" 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: 8px;">
|
<div class="cartaz-header-container" style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border-light); padding-bottom: 8px;">
|
||||||
<h4 style="margin: 0; color: var(--text-primary);">👁️ Visualização do Cartaz</h4>
|
<h4 style="margin: 0; color: var(--text-primary);">👁️ Visualização do Cartaz</h4>
|
||||||
<div style="display: flex; gap: 8px;">
|
<div class="cartaz-header-actions" style="display: flex; gap: 8px;">
|
||||||
<button id="btnCanvasModeToggle" class="btn-save-settings" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 5px 10px; font-size: 0.75rem;">🎨 Design Canvas</button>
|
<button id="btnCanvasModeToggle" class="btn-save-settings" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 5px 10px; font-size: 0.75rem;">🎨 Design Canvas</button>
|
||||||
<button id="btnEditCartazToggle" class="btn-save-settings" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 5px 10px; font-size: 0.75rem;">✏️ Editar HTML</button>
|
<button id="btnEditCartazToggle" class="btn-save-settings" style="background: transparent; border: 1px solid var(--border-light); color: var(--text-secondary); padding: 5px 10px; font-size: 0.75rem;">✏️ Editar HTML</button>
|
||||||
<button id="btnPrintCartaz" class="btn-save-settings" style="background: #3b82f6; border: none; color: white; padding: 5px 12px; font-size: 0.75rem; font-weight: bold; border-radius: 6px;">🖨️ Imprimir Cartaz</button>
|
<button id="btnPrintCartaz" class="btn-save-settings" style="background: #3b82f6; border: none; color: white; padding: 5px 12px; font-size: 0.75rem; font-weight: bold; border-radius: 6px;">🖨️ Imprimir Cartaz</button>
|
||||||
|
|||||||
@@ -1555,6 +1555,84 @@ code {
|
|||||||
padding-bottom: 16px !important;
|
padding-bottom: 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#minhaTurmaModal .settings-modal-body {
|
||||||
|
overflow-y: auto !important;
|
||||||
|
flex: none !important;
|
||||||
|
min-height: auto !important;
|
||||||
|
max-height: calc(92dvh - 120px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#turmaConfigSection, #turmaAlunosSection {
|
||||||
|
flex-direction: column !important;
|
||||||
|
overflow-y: visible !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#turmaConfigSection > div, #turmaAlunosSection > div {
|
||||||
|
border-right: none !important;
|
||||||
|
border-bottom: 1px solid var(--border-light) !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
padding-bottom: 16px !important;
|
||||||
|
flex: none !important;
|
||||||
|
height: auto !important;
|
||||||
|
width: 100% !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#turmaConfigSection > div:last-child, #turmaAlunosSection > div:last-child {
|
||||||
|
border-bottom: none !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
padding-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#turmaConfigSection select, #turmaConfigSection input,
|
||||||
|
#turmaAlunosSection select, #turmaAlunosSection input {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comics-project-row {
|
||||||
|
flex-direction: column !important;
|
||||||
|
align-items: stretch !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comics-project-row select,
|
||||||
|
.comics-project-row input,
|
||||||
|
.comics-project-row button {
|
||||||
|
width: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cartaz-preview-panel {
|
||||||
|
min-width: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
flex: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cartazLeftConfigPanel, #cartazLeftCanvasPanel {
|
||||||
|
min-width: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
padding-right: 0 !important;
|
||||||
|
flex: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cartaz-header-container {
|
||||||
|
flex-direction: column !important;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
gap: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cartaz-header-actions {
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
width: 100% !important;
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cartaz-header-actions button {
|
||||||
|
flex: 1 1 auto !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
#modelosRelatorioModal .settings-modal-body > div:last-child {
|
#modelosRelatorioModal .settings-modal-body > div:last-child {
|
||||||
padding-left: 0 !important;
|
padding-left: 0 !important;
|
||||||
padding-top: 16px !important;
|
padding-top: 16px !important;
|
||||||
@@ -3739,6 +3817,48 @@ code {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Camadas de Cena (Estúdio de Cenas em Camadas) */
|
||||||
|
.canvas-scene-layer {
|
||||||
|
padding: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-scene-layer:hover {
|
||||||
|
border-color: rgba(139, 92, 246, 0.8);
|
||||||
|
box-shadow: 0 0 10px rgba(139, 92, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-scene-layer.active-item {
|
||||||
|
border: 2.5px solid #8b5cf6 !important;
|
||||||
|
box-shadow: 0 0 14px rgba(139, 92, 246, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animação dos dots de loading */
|
||||||
|
@keyframes dot-pulse {
|
||||||
|
0%, 60%, 100% { transform: scale(1); opacity: 0.5; }
|
||||||
|
30% { transform: scale(1.4); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Botão Estúdio de Cenas */
|
||||||
|
#btnDecomposeScene:hover:not(:disabled) {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#btnDecomposeScene:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lista de camadas no painel */
|
||||||
|
#sceneLayersItems > div:hover {
|
||||||
|
background: rgba(139, 92, 246, 0.12) !important;
|
||||||
|
border-color: rgba(139, 92, 246, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -64,8 +64,9 @@ self.addEventListener('fetch', (event) => {
|
|||||||
|
|
||||||
return fetch(event.request).then((networkResponse) => {
|
return fetch(event.request).then((networkResponse) => {
|
||||||
if (networkResponse && networkResponse.status === 200) {
|
if (networkResponse && networkResponse.status === 200) {
|
||||||
|
const responseToCache = networkResponse.clone();
|
||||||
caches.open(CACHE_NAME).then((cache) => {
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
cache.put(event.request, networkResponse.clone());
|
cache.put(event.request, responseToCache);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return networkResponse;
|
return networkResponse;
|
||||||
@@ -78,8 +79,9 @@ self.addEventListener('fetch', (event) => {
|
|||||||
fetch(event.request)
|
fetch(event.request)
|
||||||
.then((networkResponse) => {
|
.then((networkResponse) => {
|
||||||
if (networkResponse && networkResponse.status === 200) {
|
if (networkResponse && networkResponse.status === 200) {
|
||||||
|
const responseToCache = networkResponse.clone();
|
||||||
caches.open(CACHE_NAME).then((cache) => {
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
cache.put(event.request, networkResponse.clone());
|
cache.put(event.request, responseToCache);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return networkResponse;
|
return networkResponse;
|
||||||
|
|||||||
@@ -1555,6 +1555,140 @@ Return ONLY the English prompt, no extra chat.`
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/cartazes/decompose-scene', requireAuth, async (req, res) => {
|
||||||
|
const { tema, titulo } = req.body;
|
||||||
|
if (!tema) {
|
||||||
|
return res.status(400).json({ error: 'Tema é obrigatório.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('[Decompose] Iniciando engenharia reversa do tema:', tema);
|
||||||
|
const agentConfig = readAgentConfig();
|
||||||
|
const agentName = agentConfig.agentName || "Kemily";
|
||||||
|
|
||||||
|
const segmentPrompt = `Você é a ${agentName}, assistente pedagógica.
|
||||||
|
Dada a descrição de uma imagem de cartaz infantil:
|
||||||
|
Tema: "${tema}"
|
||||||
|
Título: "${titulo || ''}"
|
||||||
|
|
||||||
|
Por favor, divida essa cena em componentes em camadas para que possamos montar um Canvas interativo.
|
||||||
|
Eu preciso de:
|
||||||
|
1. Um prompt para a imagem de fundo (background) sem NENHUM personagem, pessoa ou animal. Deve ser apenas o cenário de fundo vazio (ex: "an empty lawn inside a park with some trees under a blue sky, cartoon vector illustration style, no people, no animals, no text").
|
||||||
|
2. Uma lista de 2 a 3 elementos individuais importantes da cena que serão colocados como adesivos (stickers) por cima. Cada elemento deve ser descrito de forma isolada com fundo branco sólido (ex: "two happy cartoon kids holding hands, isolated on a solid white background, sticker style, no text").
|
||||||
|
|
||||||
|
Retorne a resposta estritamente no formato JSON abaixo, sem blocos de código markdown (\`\`\`json ou \`\`\$) e sem nenhum texto explicativo extra:
|
||||||
|
{
|
||||||
|
"backgroundPrompt": "descrição do fundo em inglês...",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"name": "Nome do Elemento (Ex: Crianças)",
|
||||||
|
"prompt": "descrição do elemento em inglês isolado em fundo branco..."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`;
|
||||||
|
|
||||||
|
let jsonResponse = null;
|
||||||
|
try {
|
||||||
|
const chatRes = await callMinimax({
|
||||||
|
messages: [{ role: 'user', content: segmentPrompt }],
|
||||||
|
temperature: 0.2,
|
||||||
|
max_tokens: 800
|
||||||
|
});
|
||||||
|
let text = chatRes.text.trim();
|
||||||
|
text = text.replace(/```json|```/g, '').trim();
|
||||||
|
jsonResponse = JSON.parse(text);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Decompose] Erro ao obter JSON do Minimax:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jsonResponse || !jsonResponse.backgroundPrompt || !jsonResponse.elements) {
|
||||||
|
jsonResponse = {
|
||||||
|
backgroundPrompt: `empty scene backdrop for ${tema}, cartoon vector style, no people, no animals, no text`,
|
||||||
|
elements: [
|
||||||
|
{ name: "Elemento Principal", prompt: `${tema}, cartoon vector style, isolated on a solid white background, sticker style, no text` }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Decompose] Planejamento da cena em camadas:', jsonResponse);
|
||||||
|
|
||||||
|
const minmBase = (process.env.MINIMAX_API_BASE || 'https://api.minimax.io/v1').replace(/\/v1\/?$/, '');
|
||||||
|
|
||||||
|
// Função para gerar uma imagem individual via Minimax
|
||||||
|
const generateAndDownload = async (promptText) => {
|
||||||
|
try {
|
||||||
|
console.log('[Decompose] Gerando imagem para o prompt:', promptText);
|
||||||
|
const genResp = 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: `${promptText}. Complete figures shown from top to bottom without cutoffs, kid-friendly illustration style, 3D Pixar, soft pastel colors, no text, no words, no letters, no writing, no labels.`,
|
||||||
|
n: 1
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (genResp.ok) {
|
||||||
|
const genData = await genResp.json();
|
||||||
|
const rawImgUrl = genData.data?.image_urls?.[0];
|
||||||
|
if (rawImgUrl) {
|
||||||
|
const imgFetch = await fetch(rawImgUrl);
|
||||||
|
if (imgFetch.ok) {
|
||||||
|
const imgBuffer = Buffer.from(await imgFetch.arrayBuffer());
|
||||||
|
const fileName = `layer_${Date.now()}_${Math.floor(Math.random() * 1000)}.jpg`;
|
||||||
|
const mediaDir = path.join(__dirname, 'public', 'generated-media');
|
||||||
|
if (!fs.existsSync(mediaDir)) fs.mkdirSync(mediaDir, { recursive: true });
|
||||||
|
const filePath = path.join(mediaDir, fileName);
|
||||||
|
fs.writeFileSync(filePath, imgBuffer);
|
||||||
|
await backupMediaFile(filePath, imgBuffer);
|
||||||
|
return `/generated-media/${fileName}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Decompose] Erro na geração de camada:', err);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gerar fundo e stickers paralelamente
|
||||||
|
const promises = [];
|
||||||
|
promises.push(generateAndDownload(jsonResponse.backgroundPrompt));
|
||||||
|
for (const el of jsonResponse.elements) {
|
||||||
|
promises.push(generateAndDownload(el.prompt));
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
|
||||||
|
const backgroundUrl = results[0];
|
||||||
|
const elementUrls = results.slice(1);
|
||||||
|
|
||||||
|
const layers = jsonResponse.elements.map((el, index) => {
|
||||||
|
return {
|
||||||
|
name: el.name,
|
||||||
|
url: elementUrls[index],
|
||||||
|
left: 20 + index * 25,
|
||||||
|
top: 35,
|
||||||
|
width: 30,
|
||||||
|
height: 30
|
||||||
|
};
|
||||||
|
}).filter(l => l.url !== null);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
backgroundUrl,
|
||||||
|
layers
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Decompose] Erro no desmembramento:', err);
|
||||||
|
res.status(500).json({ error: 'Erro ao desmembrar a imagem: ' + err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/api/cartazes/list', requireAuth, async (req, res) => {
|
app.get('/api/cartazes/list', requireAuth, async (req, res) => {
|
||||||
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
|
const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000';
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user