diff --git a/public/app.js b/public/app.js index b1065c2..60b62f6 100644 --- a/public/app.js +++ b/public/app.js @@ -2097,56 +2097,47 @@ const initApp = () => { const mode = btn.dataset.mode; const text = userInput.value.trim(); - // Se for modo POETRY ou TEXT, ou se o input estiver vazio, preenchemos o input com a sugestão correspondente sem enviar - const isTextOrPoetry = (mode === 'POETRY' || mode === 'TEXT'); + let suggestion = ""; + let baseText = text; - if (!text || isTextOrPoetry) { - let suggestion = ""; - let baseText = text; - - // Se o texto atual já for uma das sugestões padrão, limpamos para não duplicar - const suggestionsList = [ - "Crie uma música pedagógica alegre e animada sobre ", - "Crie um roteiro detalhado para um vídeo educativo sobre ", - "Crie uma ilustração pedagógica com cores vibrantes mostrando ", - "Escreva uma poesia infantil rimada e educativa sobre ", - "Escreva uma história educativa infantil curta sobre " - ]; - - for (const sugg of suggestionsList) { - if (baseText.startsWith(sugg)) { - baseText = baseText.replace(sugg, ""); - break; - } + // Se o texto atual já for uma das sugestões padrão, limpamos para não duplicar + const suggestionsList = [ + "Crie uma música pedagógica alegre e animada sobre ", + "Crie um roteiro detalhado para um vídeo educativo sobre ", + "Crie uma ilustração pedagógica com cores vibrantes mostrando ", + "Escreva uma poesia infantil rimada e educativa sobre ", + "Escreva uma história educativa infantil curta sobre " + ]; + + for (const sugg of suggestionsList) { + if (baseText.startsWith(sugg)) { + baseText = baseText.replace(sugg, "").trim(); + break; } - - if (mode === 'AUDIO') { - suggestion = "Crie uma música pedagógica alegre e animada sobre "; - } else if (mode === 'VIDEO') { - suggestion = "Crie um roteiro detalhado para um vídeo educativo sobre "; - } else if (mode === 'IMAGE') { - suggestion = "Crie uma ilustração pedagógica com cores vibrantes mostrando "; - } else if (mode === 'POETRY') { - suggestion = "Escreva uma poesia infantil rimada e educativa sobre "; - } else if (mode === 'TEXT') { - suggestion = "Escreva uma história educativa infantil curta sobre "; - } - - userInput.value = suggestion + baseText; - userInput.focus(); - - // Mover cursor para o final do texto - userInput.selectionStart = userInput.selectionEnd = userInput.value.length; - - // Ajustar altura do textarea para acomodar a sugestão - userInput.style.height = 'auto'; - userInput.style.height = userInput.scrollHeight + 'px'; - btnSend.disabled = false; - return; } - // Se houver texto e for outro modo (AUDIO, VIDEO, IMAGE), envia imediatamente - handleSendMessage(text, false, null, mode); + if (mode === 'AUDIO') { + suggestion = "Crie uma música pedagógica alegre e animada sobre "; + } else if (mode === 'VIDEO') { + suggestion = "Crie um roteiro detalhado para um vídeo educativo sobre "; + } else if (mode === 'IMAGE') { + suggestion = "Crie uma ilustração pedagógica com cores vibrantes mostrando "; + } else if (mode === 'POETRY') { + suggestion = "Escreva uma poesia infantil rimada e educativa sobre "; + } else if (mode === 'TEXT') { + suggestion = "Escreva uma história educativa infantil curta sobre "; + } + + userInput.value = suggestion + baseText; + userInput.focus(); + + // Mover cursor para o final do texto + userInput.selectionStart = userInput.selectionEnd = userInput.value.length; + + // Ajustar altura do textarea para acomodar a sugestão + userInput.style.height = 'auto'; + userInput.style.height = userInput.scrollHeight + 'px'; + if (btnSend) btnSend.disabled = false; }); }); @@ -3515,6 +3506,7 @@ const initApp = () => { const btnSaveComicsProject = document.getElementById('btnSaveComicsProject'); let currentComicsProjectId = null; + let currentComicsCharDescEnglish = ''; let selectedComicsQty = 5; let selectedComicsBubbles = true; let selectedComicsRatio = '16:9'; @@ -3617,6 +3609,13 @@ const initApp = () => { // --- GERENCIADOR DE PROJETOS DE HQ --- const startNewComicsProject = () => { currentComicsProjectId = null; + currentComicsCharDescEnglish = ''; + + const modeContainer = document.getElementById('comicsGenerationModeContainer'); + if (modeContainer) modeContainer.style.display = 'none'; + const radioNew = document.querySelector('input[name="comicsGenMode"][value="new_story"]'); + if (radioNew) radioNew.checked = true; + if (comicsProjectSelect) comicsProjectSelect.value = ''; if (comicsProjectTitleInput) comicsProjectTitleInput.value = ''; if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'none'; @@ -3699,6 +3698,11 @@ const initApp = () => { const { project, panels } = await resp.json(); currentComicsProjectId = project.id; + currentComicsCharDescEnglish = project.character_description_english || ''; + + const modeContainer = document.getElementById('comicsGenerationModeContainer'); + if (modeContainer) modeContainer.style.display = 'flex'; + if (comicsProjectTitleInput) comicsProjectTitleInput.value = project.titulo || ''; comicsTemaInput.value = project.tema || ''; @@ -3852,9 +3856,10 @@ const initApp = () => { cenario, proporcao: selectedComicsRatio, character_description_global: JSON.stringify(characters), + character_description_english: currentComicsCharDescEnglish || '', panels: generatedPanelsData.map(p => ({ panel_number: p.panel_number, - image_url: p.imageUrl, + image_url: p.imageUrl || p.image_url || '', image_prompt: p.image_prompt || '', dialogue: p.dialogue || '' })) @@ -4009,17 +4014,32 @@ const initApp = () => { try { // 1. Gerar Roteiro + const genMode = currentComicsProjectId ? (document.querySelector('input[name="comicsGenMode"]:checked')?.value || 'new_story') : 'new_story'; + + const requestBody = { + tema, + quantidade: selectedComicsQty, + baloes: selectedComicsBubbles, + proporcao: selectedComicsRatio, + personagens, + cenario + }; + + if (currentComicsProjectId) { + requestBody.continuationType = genMode; + requestBody.character_description = currentComicsCharDescEnglish; + requestBody.existingPanels = generatedPanelsData.map(p => ({ + panel_number: p.panel_number, + image_url: p.imageUrl || p.image_url || '', + image_prompt: p.image_prompt || '', + dialogue: p.dialogue || '' + })); + } + const scriptResp = await fetch('/api/comics/generate-script', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - tema, - quantidade: selectedComicsQty, - baloes: selectedComicsBubbles, - proporcao: selectedComicsRatio, - personagens, - cenario - }) + body: JSON.stringify(requestBody) }); if (!scriptResp.ok) { @@ -4030,8 +4050,14 @@ const initApp = () => { const scriptData = await scriptResp.json(); comicsLoaderProgress.style.width = '15%'; + if (scriptData.character_description) { + currentComicsCharDescEnglish = scriptData.character_description; + } + // 2. Gerar cada quadrinho sequencialmente - generatedPanelsData = []; + if (genMode !== 'extend') { + generatedPanelsData = []; + } const totalPanels = scriptData.panels.length; for (let i = 0; i < totalPanels; i++) { @@ -4058,12 +4084,13 @@ const initApp = () => { generatedPanelsData.push({ panel_number: panel.panel_number, imageUrl: frameData.imageUrl, - dialogue: panel.dialogue || '' + dialogue: panel.dialogue || '', + image_prompt: panel.image_prompt || '' }); } - // Renderizar quadrinhos na tela - comicsResultTitle.textContent = scriptData.title || 'Fábrica de Quadrinhos Camila'; + // Renderizar quadrinhos na tela (todos, incluindo os anteriores caso tenha sido extensão) + comicsResultTitle.textContent = scriptData.title || comicsResultTitle.textContent || 'Fábrica de Quadrinhos Camila'; comicsGridContainer.innerHTML = ''; generatedPanelsData.forEach(panel => { @@ -4075,7 +4102,7 @@ const initApp = () => { const img = document.createElement('img'); img.className = 'comic-panel-image'; - img.src = panel.imageUrl; + img.src = panel.imageUrl || panel.image_url; img.alt = `Quadro ${panel.panel_number}`; imgContainer.appendChild(img); @@ -4106,6 +4133,18 @@ const initApp = () => { comicsGridContainer.appendChild(panelCard); }); + // 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) { + currentComicsProjectId = null; + if (comicsProjectSelect) comicsProjectSelect.value = ''; + const modeContainer = document.getElementById('comicsGenerationModeContainer'); + if (modeContainer) modeContainer.style.display = 'none'; + if (comicsProjectTitleInput) { + comicsProjectTitleInput.value = scriptData.title || (comicsProjectTitleInput.value + ' (Derivado)'); + } + if (btnDeleteComicsProject) btnDeleteComicsProject.style.display = 'none'; + } + comicsGenerateLoader.style.display = 'none'; comicsResultArea.style.display = 'flex'; btnGenerateComics.disabled = false; diff --git a/public/index.html b/public/index.html index 4a646e5..8c24b83 100644 --- a/public/index.html +++ b/public/index.html @@ -761,6 +761,21 @@ + + + diff --git a/server.js b/server.js index 30ab24a..c4e3d29 100644 --- a/server.js +++ b/server.js @@ -365,7 +365,7 @@ Gere um JSON perfeitamente válido contendo: "panels": [ { "panel_number": ${startPanelNum}, - "image_prompt": "Prompt visual detalhado em inglês para o gerador de imagens da MiniMax descrevendo a cena deste quadrinho. IMPORTANTE: Use enquadramento baixo/afastado (wide angle, medium shot, characters seen from a distance, low framing). O prompt DEVE conter a instrução explícita em inglês de deixar o terço superior (cerca de 30% do topo da imagem) totalmente livre de personagens (cabeças, etc) ou elementos cruciais da cena, mantendo-o como espaço vazio/fundo limpo (ex: 'lower framing, characters positioned in the middle-to-lower section, leaving the top 30% of the image as empty space / simple clean background for text overlay'), para que os balões de diálogo caibam por cima sem ocultar os personagens. O estilo deve ser de ilustração de livro infantil de alta qualidade, 3D Pixar, traço limpo. Você DEVE incluir e respeitar estritamente a descrição visual física de cada personagem participante neste quadrinho conforme definido nos dados do projeto existente acima, para evitar qualquer alucinação de roupas, cores ou visual físico.", + "image_prompt": "Prompt visual detalhado em inglês para o gerador de imagens da MiniMax descrevendo a cena deste quadrinho. IMPORTANTE PARA CONSISTÊNCIA: Substitua TODOS os nomes de personagens por sua descrição física minuciosa em inglês (obtida de character_description) em cada quadrinho que aparecerem (ex: em vez de apenas escrever 'Lucas', escreva a descrição detalhada dele com roupas e cabelos específicos, etc). IMPORTANTE PARA TEXTO: Use enquadramento baixo/afastado (wide angle, medium shot, characters seen from a distance, low framing). O prompt DEVE conter a instrução explícita em inglês de deixar o terço superior (cerca de 30% do topo da imagem) totalmente livre de personagens ou elementos cruciais da cena, mantendo-o como espaço vazio/fundo limpo (ex: 'lower framing, characters positioned in the middle-to-lower section, leaving the top 30% of the image as empty space / simple clean background for text overlay'), para os balões de diálogo. O estilo deve ser de ilustração de livro infantil de alta qualidade, 3D Pixar, traço limpo.", "dialogue": "Falas do quadrinho em português (ex: 'Lucas: Veja aquilo!') com NO MÁXIMO 10 palavras, ou narração curta caso não tenha balões." }, ... @@ -388,7 +388,7 @@ Gere um JSON perfeitamente válido contendo: "panels": [ { "panel_number": 1, - "image_prompt": "Prompt visual detalhado em inglês para o gerador de imagens da MiniMax descrevendo a cena deste quadrinho. IMPORTANTE: Use enquadramento baixo/afastado (wide angle, medium shot, characters seen from a distance, low framing). O prompt DEVE conter a instrução explícita em inglês de deixar o terço superior (cerca de 30% do topo da imagem) totalmente livre de personagens (cabeças, etc) ou elementos cruciais da cena, mantendo-o como espaço vazio/fundo limpo (ex: 'lower framing, characters positioned in the middle-to-lower section, leaving the top 30% of the image as empty space / simple clean background for text overlay'), para que os balões de diálogo caibam por cima sem ocultar os personagens. Ex: 'Wide angle shot, Lucas and Mariana standing in a colorful sunny park, lower framing, characters positioned in the middle-to-lower section, leaving the top 30% of the image as empty sky for text overlay. ...'. Deve incluir a descrição física constante de cada personagem participante neste quadrinho. O estilo deve ser de ilustração de livro infantil de alta qualidade, 3D Pixar, traço limpo.", + "image_prompt": "Prompt visual detalhado em inglês para o gerador de imagens da MiniMax descrevendo a cena deste quadrinho. IMPORTANTE PARA CONSISTÊNCIA: Substitua TODOS os nomes de personagens por sua descrição física minuciosa em inglês (do character_description) em cada quadrinho que aparecerem (ex: não escreva apenas 'Mariana', escreva a descrição dela detalhando cabelo e roupas de forma fixa e idêntica em todos os quadrinhos). IMPORTANTE PARA TEXTO: Use enquadramento baixo/afastado (wide angle, medium shot, characters seen from a distance, low framing) e instrua em inglês a deixar o terço superior (30% do topo da imagem) totalmente livre de personagens ou elementos cruciais (ex: 'lower framing, characters positioned in the middle-to-lower section, leaving the top 30% of the image as empty space / simple clean background for text overlay'), para os balões de diálogo. O estilo deve ser de ilustração de livro infantil de alta qualidade, 3D Pixar, traço limpo.", "dialogue": "Falas do quadrinho em português (ex: 'Lucas: Veja aquilo!') com NO MÁXIMO 10 palavras, ou narração curta caso não tenha balões." }, ... @@ -568,7 +568,7 @@ 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;', + 'SELECT id, titulo, tema, cenario, proporcao, character_description_global, character_description_english, created_at, updated_at FROM escola.projetos_comics WHERE usuario_id = $1 ORDER BY updated_at DESC;', [usuarioId] ); res.json(result.rows); @@ -609,7 +609,7 @@ app.get('/api/comics/projects/:id', requireAuth, async (req, res) => { // 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; + const { id, titulo, tema, cenario, proporcao, character_description_global, character_description_english, panels } = req.body; if (!titulo) { return res.status(400).json({ error: 'O título do projeto é obrigatório.' }); @@ -633,17 +633,17 @@ app.post('/api/comics/projects', requireAuth, async (req, res) => { // 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] + SET titulo = $1, tema = $2, cenario = $3, proporcao = $4, character_description_global = $5, character_description_english = $6, updated_at = NOW() + WHERE id = $7;`, + [titulo, tema, cenario, proporcao, character_description_global, character_description_english, 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) + `INSERT INTO escola.projetos_comics (usuario_id, titulo, tema, cenario, proporcao, character_description_global, character_description_english) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id;`, - [usuarioId, titulo, tema, cenario, proporcao, character_description_global] + [usuarioId, titulo, tema, cenario, proporcao, character_description_global, character_description_english] ); projetoId = insertProj.rows[0].id; } @@ -872,6 +872,9 @@ async function initDatabase() { ); `); + // Garantir que a coluna character_description_english exista na tabela projetos_comics + await dbPool.query(`ALTER TABLE escola.projetos_comics ADD COLUMN IF NOT EXISTS character_description_english TEXT;`); + // 0.1. Garantir que a tabela de modelos exista e tenha dados padrão await dbPool.query(` CREATE TABLE IF NOT EXISTS escola.modelos (