Spaces:
Sleeping
Sleeping
<html lang="pt-br"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Sistema Multi-Agente de IA</title> | |
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> | |
<style> | |
/* Estilos para o novo botão de conversão */ | |
.convert-btn { | |
padding: 5px 10px; | |
font-size: 12px; | |
background-color: #17a2b8; | |
color: white; | |
border: none; | |
border-radius: 4px; | |
cursor: pointer; | |
margin-left: 5px; | |
} | |
.convert-btn:hover { | |
background-color: #138496; | |
} | |
.convert-btn:disabled { | |
background-color: #5a6268; | |
cursor: not-allowed; | |
} | |
/* Estilos para garantir que o texto final fique abaixo das 3 colunas */ | |
.results-wrapper { | |
display: flex; | |
flex-direction: column; | |
gap: 20px; | |
} | |
.results-container { | |
display: none; | |
flex-direction: row; | |
gap: 10px; | |
margin-bottom: 0; | |
} | |
.results-container .result-column { | |
flex: 1; | |
min-width: 0; | |
} | |
/* ESTILOS RESPONSIVOS */ | |
/* Header responsivo */ | |
.header-container { | |
display: flex; | |
flex-direction: column; | |
gap: 15px; | |
margin-bottom: 20px; | |
} | |
.controls-container { | |
display: flex; | |
flex-wrap: wrap; | |
gap: 10px; | |
align-items: center; | |
justify-content: center; | |
} | |
/* Mode toggles responsivos */ | |
.mode-toggle { | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
font-size: 14px; | |
white-space: nowrap; | |
} | |
/* Botão refresh responsivo */ | |
.refresh-btn { | |
padding: 8px 12px; | |
font-size: 14px; | |
white-space: nowrap; | |
} | |
/* Estilos para os campos de tamanho do texto - RESPONSIVO */ | |
.text-size-controls { | |
display: flex; | |
flex-direction: row; | |
gap: 15px; | |
margin: 15px 0; | |
align-items: flex-start; | |
background-color: #f8f9fa; | |
padding: 15px; | |
border-radius: 8px; | |
border: 1px solid #e9ecef; | |
flex-wrap: wrap; | |
} | |
.text-size-field { | |
display: flex; | |
flex-direction: column; | |
gap: 5px; | |
min-width: 120px; | |
} | |
.text-size-field label { | |
font-size: 12px; | |
color: #6c757d; | |
font-weight: bold; | |
} | |
.text-size-field input { | |
width: 100%; | |
max-width: 120px; | |
padding: 8px 10px; | |
border: 1px solid #ced4da; | |
border-radius: 4px; | |
font-size: 14px; | |
} | |
.text-size-tip { | |
flex: 1; | |
min-width: 100%; | |
font-size: 14px; | |
color: #ffffff; | |
line-height: 1.4; | |
} | |
/* Estilo para o botão de cancelar */ | |
.cancel-btn { | |
background-color: #dc3545; | |
color: white; | |
border: none; | |
padding: 10px 20px; | |
border-radius: 6px; | |
cursor: pointer; | |
margin-top: 15px; | |
font-size: 14px; | |
transition: background-color 0.3s; | |
} | |
.cancel-btn:hover { | |
background-color: #c82333; | |
} | |
.cancel-btn:disabled { | |
background-color: #6c757d; | |
cursor: not-allowed; | |
} | |
/* Ajuste no loader para acomodar o botão de cancelar */ | |
.loader-content { | |
text-align: center; | |
padding: 20px; | |
} | |
/* Resultados responsivos */ | |
.results-container { | |
gap: 15px; | |
} | |
.result-column { | |
min-width: 0; | |
overflow: hidden; | |
} | |
.column-header { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
flex-wrap: wrap; | |
gap: 10px; | |
padding: 12px 15px; | |
background-color: #f8f9fa; | |
border-bottom: 1px solid #dee2e6; | |
} | |
.column-header h2 { | |
margin: 0; | |
font-size: 18px; | |
color: #495057; | |
} | |
.column-header div { | |
display: flex; | |
gap: 8px; | |
flex-wrap: wrap; | |
} | |
/* Botões responsivos */ | |
.copy-btn, .convert-btn { | |
padding: 6px 12px; | |
font-size: 12px; | |
border: none; | |
border-radius: 4px; | |
cursor: pointer; | |
transition: background-color 0.3s; | |
white-space: nowrap; | |
} | |
.copy-btn { | |
background-color: #28a745; | |
color: white; | |
} | |
.copy-btn:hover { | |
background-color: #218838; | |
} | |
/* Output boxes responsivos - SEM BARRA DE ROLAGEM INDIVIDUAL */ | |
.output-box { | |
padding: 15px; | |
border: 1px solid #dee2e6; | |
border-top: none; | |
background-color: #ffffff; | |
min-height: 300px; | |
/* REMOVIDO: max-height e overflow-y */ | |
font-size: 14px; | |
line-height: 1.6; | |
word-wrap: break-word; | |
white-space: pre-wrap; | |
} | |
/* Form elements responsivos */ | |
textarea, input[type="text"], input[type="number"] { | |
width: 100%; | |
box-sizing: border-box; | |
font-size: 14px; | |
} | |
button[type="submit"] { | |
width: 100%; | |
padding: 12px; | |
font-size: 16px; | |
background-color: #007bff; | |
color: white; | |
border: none; | |
border-radius: 6px; | |
cursor: pointer; | |
transition: background-color 0.3s; | |
} | |
button[type="submit"]:hover { | |
background-color: #0056b3; | |
} | |
/* MEDIA QUERIES PARA DIFERENTES TAMANHOS DE TELA */ | |
/* Tablets e telas médias */ | |
@media (max-width: 992px) { | |
.container { | |
padding: 15px; | |
} | |
.results-container { | |
flex-direction: column; | |
gap: 20px; | |
} | |
.result-column { | |
width: 100%; | |
} | |
.text-size-controls { | |
flex-direction: column; | |
gap: 15px; | |
align-items: stretch; | |
} | |
.text-size-field { | |
min-width: 100%; | |
} | |
.text-size-tip { | |
min-width: 100%; | |
} | |
} | |
/* Smartphones */ | |
@media (max-width: 768px) { | |
.container { | |
padding: 10px; | |
} | |
.header-container { | |
text-align: center; | |
} | |
.header-container h1 { | |
font-size: 24px; | |
margin-bottom: 5px; | |
} | |
.controls-container { | |
flex-direction: column; | |
gap: 15px; | |
} | |
.mode-toggle { | |
font-size: 16px; | |
gap: 10px; | |
} | |
.mode-toggle span { | |
min-width: 50px; | |
text-align: center; | |
} | |
.switch { | |
transform: scale(1.2); | |
margin-right: 10px; | |
} | |
.refresh-btn { | |
padding: 10px 20px; | |
font-size: 16px; | |
} | |
.text-size-controls { | |
padding: 12px; | |
gap: 12px; | |
} | |
.text-size-field input { | |
max-width: 100%; | |
padding: 10px; | |
font-size: 16px; /* Evita zoom no iOS */ | |
} | |
.column-header { | |
flex-direction: column; | |
align-items: stretch; | |
text-align: center; | |
gap: 8px; | |
padding: 15px; | |
} | |
.column-header h2 { | |
font-size: 20px; | |
margin-bottom: 8px; | |
} | |
.column-header div { | |
justify-content: center; | |
gap: 10px; | |
} | |
.copy-btn, .convert-btn { | |
padding: 8px 16px; | |
font-size: 14px; | |
flex: 1; | |
min-width: 100px; | |
} | |
.output-box { | |
font-size: 16px; | |
padding: 12px; | |
/* REMOVIDO: max-height */ | |
} | |
.cancel-btn { | |
padding: 12px 24px; | |
font-size: 16px; | |
} | |
/* Ajustes específicos para o merge button */ | |
.floating-merge-btn { | |
position: fixed; | |
bottom: 20px; | |
right: 20px; | |
padding: 15px 20px; | |
font-size: 16px; | |
border-radius: 8px; | |
box-shadow: 0 4px 12px rgba(0,0,0,0.3); | |
z-index: 1000; | |
} | |
} | |
/* Smartphones muito pequenos */ | |
@media (max-width: 480px) { | |
.container { | |
padding: 8px; | |
} | |
.header-container h1 { | |
font-size: 20px; | |
} | |
.text-size-controls { | |
padding: 10px; | |
} | |
.column-header { | |
padding: 10px; | |
} | |
.column-header h2 { | |
font-size: 18px; | |
} | |
.copy-btn, .convert-btn { | |
padding: 10px; | |
font-size: 12px; | |
min-width: 80px; | |
} | |
.output-box { | |
padding: 10px; | |
font-size: 14px; | |
/* REMOVIDO: max-height */ | |
} | |
.floating-merge-btn { | |
bottom: 10px; | |
right: 10px; | |
padding: 12px 16px; | |
font-size: 14px; | |
} | |
} | |
/* Ajustes para modo landscape em mobile */ | |
@media (max-height: 500px) and (orientation: landscape) { | |
/* REMOVIDO: ajuste de max-height dos output-box */ | |
.loader-content { | |
padding: 10px; | |
} | |
.cancel-btn { | |
padding: 8px 16px; | |
margin-top: 10px; | |
} | |
} | |
/* Melhorias de acessibilidade */ | |
@media (prefers-reduced-motion: reduce) { | |
*, *::before, *::after { | |
animation-duration: 0.01ms ; | |
animation-iteration-count: 1 ; | |
transition-duration: 0.01ms ; | |
} | |
} | |
/* Dark mode support */ | |
@media (prefers-color-scheme: dark) { | |
.text-size-controls { | |
background-color: #343a40; | |
border-color: #495057; | |
color: #f8f9fa; | |
} | |
.text-size-field label { | |
color: #adb5bd; | |
} | |
.text-size-tip { | |
color: #adb5bd; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div id="loader-overlay" style="display: none;"> | |
<div class="loader-content"> | |
<div class="loader-spinner"></div> | |
<p id="loader-message">Processando sua solicitação...</p> | |
<div class="progress-bar-container"><div id="progress-bar" class="progress-bar"></div></div> | |
<button id="cancel-btn" class="cancel-btn">Cancelar Processamento</button> | |
</div> | |
</div> | |
<button id="merge-btn" class="floating-merge-btn" style="display: none;">Processar Merge</button> | |
<div class="container"> | |
<div class="header-container"> | |
<div> | |
<h1>Sistema Multi-Agente IA</h1> | |
<p id="flow-description">OPEN AI ➔ Claude Sonnet ➔ Gemini</p> | |
</div> | |
<div class="controls-container"> | |
<div class="mode-toggle" title="A versão 'Hierárquica' gerará um único texto que passará por revisão em duas instâncias. Na versão 'Atômica', serão gerados 3 textos, um em cada modelo de IA; e depois um 4º texto será gerado fazendo um texto final consolidado dessas 3 versões."> | |
<span>Hierárquico</span> | |
<label class="switch"><input type="checkbox" id="processing-mode-switch"><span class="slider round"></span></label> | |
<span>Atômico</span> | |
</div> | |
<div class="mode-toggle"> | |
<span>Modo Real</span> | |
<label class="switch"><input type="checkbox" id="mode-switch"><span class="slider round"></span></label> | |
<span>Modo Teste</span> | |
</div> | |
<button class="refresh-btn" onclick="window.location.href='/'" title="Limpar e começar de novo">Nova Consulta</button> | |
</div> | |
</div> | |
<div id="error-box-container"></div> | |
<div id="real-form-container"> | |
<form id="request-form-real"> | |
<div class="text-contexto_usuario"> | |
<label for="contexto_usuario">Contexto:</label> | |
<textarea id="contexto_usuario" name="contexto" rows="3" placeholder="Você é um filósofo e teólogo católico, especialista em redigir textos profundos e detalhados sobre assuntos diversos da filosofia, teologia, política, antropologia, educação, psicologia etc."></textarea> | |
</div> | |
<label for="solicitacao_usuario">Digite sua solicitação (ou arraste arquivos aqui):</label> | |
<textarea name="solicitacao_usuario" id="solicitacao_usuario" rows="8" required></textarea> | |
<div id="file-list-container"><p>Arquivos Anexados:</p><ul id="file-list"></ul></div> | |
<!-- CAMPOS DE TAMANHO DO TEXTO - RESPONSIVOS --> | |
<div class="text-size-controls"> | |
<div class="text-size-field"> | |
<label for="min_chars">Mín. Caracteres:</label> | |
<input type="number" id="min_chars" name="min_chars" value="24000" min="1000" max="100000" step="1000"> | |
</div> | |
<div class="text-size-field"> | |
<label for="max_chars">Máx. Caracteres:</label> | |
<input type="number" id="max_chars" name="max_chars" value="30000" min="1000" max="100000" step="1000"> | |
</div> | |
<div class="text-size-tip"> | |
<strong>Dica:</strong> Defina o tamanho desejado para os textos gerados pelas IAs. Cada 1.000 caracteres representam, em média, 200 palavras. Valores maiores resultam em textos mais detalhados. | |
</div> | |
</div> | |
<!-- Melhoria 05/08/2025 - Multi-modelo --> | |
<div id="modelo-container" style="display:none;"> | |
<label>Escolha os modelos:</label> | |
<div> | |
<input type="checkbox" id="modelo-openai" name="modelo-openai" checked> | |
<label for="modelo-openai">OpenAI</label> | |
<input type="checkbox" id="modelo-sonnet" name="modelo-sonnet" checked> | |
<label for="modelo-sonnet">Sonnet</label> | |
<input type="checkbox" id="modelo-gemini" name="modelo-gemini" checked> | |
<label for="modelo-gemini">Gemini</label> | |
</div> | |
</div> | |
<button type="submit">Processar com IA</button> | |
</form> | |
</div> | |
<div id="mock-form-container" style="display: none;"> | |
<form id="request-form-mock"> | |
<label for="mock_text">Cole o texto de simulação aqui:</label> | |
<textarea name="mock_text" id="mock_text" rows="10" required>### Título | |
Este é um exemplo de texto **bruto** em Markdown. | |
- Item 1 | |
- Item 2 | |
Use o botão `Converter para MD` para ver a mágica.</textarea> | |
<button type="submit">Simular Resposta</button> | |
</form> | |
</div> | |
<!-- WRAPPER PRINCIPAL PARA OS RESULTADOS --> | |
<div class="results-wrapper"> | |
<!-- AS 3 COLUNAS PRINCIPAIS --> | |
<div id="results-container" class="results-container"> | |
<div class="result-column"> | |
<div class="column-header"> | |
<h2>OPEN AI</h2> | |
<div> | |
<button class="copy-btn" onclick="copyToClipboard('openai-output')">Copiar</button> | |
<button class="convert-btn" onclick="convertToMarkdown('openai-output')">Converter para MD</button> | |
</div> | |
</div> | |
<div class="output-box" id="openai-output"></div> | |
</div> | |
<div class="result-column"> | |
<div class="column-header"> | |
<h2>Claude Sonnet</h2> | |
<div> | |
<button class="copy-btn" onclick="copyToClipboard('sonnet-output')">Copiar</button> | |
<button class="convert-btn" onclick="convertToMarkdown('sonnet-output')">Converter para MD</button> | |
</div> | |
</div> | |
<div class="output-box" id="sonnet-output"></div> | |
</div> | |
<div class="result-column"> | |
<div class="column-header"> | |
<h2>Gemini</h2> | |
<div> | |
<button class="copy-btn" onclick="copyToClipboard('gemini-output')">Copiar</button> | |
<button class="convert-btn" onclick="convertToMarkdown('gemini-output')">Converter para MD</button> | |
</div> | |
</div> | |
<div class="output-box" id="gemini-output"></div> | |
</div> | |
</div> | |
<!-- TEXTO FINAL ABAIXO DAS 3 COLUNAS --> | |
<div id="final-result-container"> | |
<div class="column-header" style="border-radius: 8px 8px 0 0; background-color: #e9ecef;"> | |
<h2 id="final-result-title">Texto Final</h2> | |
<div> | |
<button class="copy-btn" onclick="copyToClipboard('final-output')">Copiar</button> | |
<button class="convert-btn" onclick="convertToMarkdown('final-output')">Converter para MD</button> | |
</div> | |
</div> | |
<div class="output-box" id="final-output" style="background-color: #fafafa; border: 1px solid #e0e0e0; border-top: none; border-radius: 0 0 8px 8px;"></div> | |
</div> | |
</div> | |
</div> | |
<script> | |
// --- Variáveis Globais --- | |
const processingModeSwitch = document.getElementById('processing-mode-switch'); | |
// Melhoria 05/08/2025 - Multi-modelo | |
const modeloContainer = document.getElementById('modelo-container'); | |
modeloContainer.style.display = this.checked ? 'block' : 'none'; | |
const modeSwitch = document.getElementById('mode-switch'); | |
const realContainer = document.getElementById('real-form-container'); | |
const mockContainer = document.getElementById('mock-form-container'); | |
const loader = document.getElementById('loader-overlay'); | |
const loaderMessage = document.getElementById('loader-message'); | |
const progressBar = document.getElementById('progress-bar'); | |
const resultsContainer = document.getElementById('results-container'); | |
const errorContainer = document.getElementById('error-box-container'); | |
const contextoField = document.getElementById('contexto_usuario'); | |
const textarea = document.getElementById('solicitacao_usuario'); | |
const fileList = document.getElementById('file-list'); | |
const mergeBtn = document.getElementById('merge-btn'); | |
const finalResultContainer = document.getElementById('final-result-container'); | |
const finalOutput = document.getElementById('final-output'); | |
const cancelBtn = document.getElementById('cancel-btn'); | |
let attachedFiles = []; | |
let originalUserQuery = ""; | |
let rawTexts = {}; | |
let currentProcessingType = null; | |
// Log para debug | |
function debugLog(message) { | |
console.log(`[FRONTEND DEBUG] ${message}`); | |
} | |
// --- Lógica de UI --- | |
modeSwitch.addEventListener('change', function() { | |
realContainer.style.display = this.checked ? 'none' : 'block'; | |
mockContainer.style.display = this.checked ? 'block' : 'none'; | |
}); | |
// Melhoria 05/08/2025 - Multi-modelo | |
processingModeSwitch.addEventListener('change', function() { | |
const isAtomic = this.checked; | |
// Melhoria 05/08/2025 - Multi-modelo (Corrigido) | |
const modeloContainer = document.getElementById('modelo-container'); | |
modeloContainer.style.display = isAtomic ? 'block' : 'none'; | |
document.getElementById('flow-description').textContent = isAtomic ? | |
"OPEN AI | Claude Sonnet | Gemini (Paralelo)" : | |
"OPEN AI ➔ Claude Sonnet ➔ Gemini"; | |
}); | |
// --- Lógica do botão de cancelar --- | |
cancelBtn.addEventListener('click', async function() { | |
debugLog("=== CANCELAR BUTTON CLICADO ==="); | |
try { | |
const response = await fetch('/cancel', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' } | |
}); | |
if (response.ok) { | |
debugLog("Cancelamento solicitado com sucesso"); | |
loaderMessage.textContent = 'Cancelando processamento...'; | |
cancelBtn.disabled = true; | |
cancelBtn.textContent = 'Cancelando...'; | |
} else { | |
debugLog("Erro ao solicitar cancelamento"); | |
showError("Erro ao cancelar processamento."); | |
} | |
} catch (error) { | |
debugLog(`Erro no cancelamento: ${error.message}`); | |
showError("Erro ao cancelar processamento."); | |
} | |
}); | |
// --- Validação dos campos de tamanho --- | |
document.getElementById('min_chars').addEventListener('change', function() { | |
const minValue = parseInt(this.value); | |
const maxValue = parseInt(document.getElementById('max_chars').value); | |
if (minValue >= maxValue) { | |
document.getElementById('max_chars').value = minValue + 1000; | |
} | |
}); | |
document.getElementById('max_chars').addEventListener('change', function() { | |
const maxValue = parseInt(this.value); | |
const minValue = parseInt(document.getElementById('min_chars').value); | |
if (maxValue <= minValue) { | |
document.getElementById('min_chars').value = maxValue - 1000; | |
} | |
}); | |
// --- Lógica de Upload --- | |
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
textarea.addEventListener(eventName, preventDefaults, false); | |
document.body.addEventListener(eventName, preventDefaults, false); | |
}); | |
['dragenter', 'dragover'].forEach(eventName => textarea.addEventListener(eventName, () => textarea.classList.add('drag-over'), false)); | |
['dragleave', 'drop'].forEach(eventName => textarea.addEventListener(eventName, () => textarea.classList.remove('drag-over'), false)); | |
textarea.addEventListener('drop', handleDrop, false); | |
function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); } | |
function handleDrop(e) { handleFiles(e.dataTransfer.files); } | |
function handleFiles(files) { | |
[...files].forEach(file => { | |
const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain']; | |
if (!allowedTypes.includes(file.type)) { showError(`Formato não suportado: ${file.name}`); return; } | |
if (file.size > 100 * 1024 * 1024) { showError(`Arquivo muito grande: ${file.name}`); return; } | |
attachedFiles.push(file); | |
}); | |
updateFileList(); | |
} | |
function updateFileList() { | |
fileList.innerHTML = ''; | |
document.getElementById('file-list-container').style.display = attachedFiles.length > 0 ? 'block' : 'none'; | |
attachedFiles.forEach((file, index) => { | |
const li = document.createElement('li'); | |
li.textContent = `${file.name} (${(file.size / 1024 / 1024).toFixed(2)} MB)`; | |
const removeBtn = document.createElement('span'); | |
removeBtn.textContent = '×'; | |
removeBtn.className = 'remove-file-btn'; | |
removeBtn.onclick = () => { attachedFiles.splice(index, 1); updateFileList(); }; | |
li.appendChild(removeBtn); | |
fileList.appendChild(li); | |
}); | |
} | |
// --- Lógica de Submissão Principal --- | |
document.getElementById('request-form-real').addEventListener('submit', handleFormSubmit); | |
document.getElementById('request-form-mock').addEventListener('submit', handleFormSubmit); | |
async function handleFormSubmit(event) { | |
console.clear(); | |
event.preventDefault(); | |
debugLog("=== FORM SUBMIT INICIADO ==="); | |
currentProcessingType = 'main'; | |
// Resetar a interface | |
errorContainer.innerHTML = ''; | |
resultsContainer.style.display = 'none'; | |
finalResultContainer.style.display = 'none'; | |
mergeBtn.style.display = 'none'; | |
document.querySelectorAll('.output-box').forEach(box => box.innerHTML = ''); | |
document.querySelectorAll('.convert-btn').forEach(btn => { | |
btn.disabled = false; | |
btn.innerText = 'Converter para MD'; | |
}); | |
rawTexts = {}; | |
debugLog("Interface resetada"); | |
// Iniciar o loader | |
loaderMessage.textContent = 'Iniciando conexão...'; | |
progressBar.style.width = '0%'; | |
loader.style.display = 'flex'; | |
cancelBtn.disabled = false; | |
cancelBtn.textContent = 'Cancelar Processamento'; | |
debugLog("Loader iniciado"); | |
const formData = new FormData(); | |
formData.append('contexto', contextoField.value.trim()); | |
formData.append('processing_mode', processingModeSwitch.checked ? 'atomic' : 'hierarchical'); | |
// Adicionar parâmetros de tamanho | |
formData.append('min_chars', document.getElementById('min_chars').value); | |
formData.append('max_chars', document.getElementById('max_chars').value); | |
// Melhoria 05/08/2025 - Multi-modelo: envia os check-boxes ao servidor | |
if (processingModeSwitch.checked) { | |
['openai','sonnet','gemini'].forEach(m => { | |
const id = `modelo-${m}`; | |
if (document.getElementById(id).checked) { | |
formData.append(id, 'on'); | |
} | |
}); | |
} | |
if (modeSwitch.checked) { | |
formData.append('mode', 'test'); | |
formData.append('mock_text', document.getElementById('mock_text').value); | |
originalUserQuery = "Simulação de teste."; | |
debugLog("Modo teste configurado"); | |
} else { | |
formData.append('mode', 'real'); | |
originalUserQuery = document.getElementById('solicitacao_usuario').value; | |
formData.append('solicitacao', originalUserQuery); | |
attachedFiles.forEach(file => { formData.append('files', file); }); | |
debugLog(`Modo real configurado. Query: ${originalUserQuery.substring(0, 100)}...`); | |
} | |
try { | |
debugLog("=== INICIANDO FETCH ==="); | |
const response = await fetch('/process', { method: 'POST', body: formData }); | |
if (!response.ok || !response.body) throw new Error(`Erro na resposta do servidor: ${response.statusText}`); | |
debugLog("=== FETCH REALIZADO COM SUCESSO, INICIANDO STREAM ==="); | |
const reader = response.body.getReader(); | |
const decoder = new TextDecoder(); | |
let buffer = ''; | |
while (true) { | |
const { done, value } = await reader.read(); | |
if (done) { | |
debugLog("=== STREAM FINALIZADO ==="); | |
break; | |
} | |
const chunk = decoder.decode(value, { stream: true }); | |
buffer += chunk; | |
debugLog(`Chunk recebido: ${chunk.length} chars`); | |
let lines = buffer.split('\n\n'); | |
buffer = lines.pop(); | |
lines.forEach(line => { | |
if (line.startsWith('data: ')) { | |
const jsonData = line.substring(6).trim(); | |
if (jsonData) { | |
debugLog(`JSON recebido: ${jsonData.substring(0, 200)}...`); | |
try { | |
const data = JSON.parse(jsonData); | |
debugLog(`Dados processados: ${JSON.stringify({progress: data.progress, message: data.message, hasPartialResult: !!data.partial_result, error: data.error})}`); | |
processStreamData(data, false); | |
} catch (e) { | |
console.error("Erro ao parsear JSON do stream:", jsonData.substring(0, 200)); | |
console.error("Erro:", e); | |
} | |
} | |
} | |
}); | |
} | |
if (buffer.trim()) { | |
debugLog(`Buffer final: ${buffer}`); | |
if (buffer.startsWith('data: ')) { | |
const jsonData = buffer.substring(6).trim(); | |
if (jsonData) { | |
try { | |
const data = JSON.parse(jsonData); | |
processStreamData(data, false); | |
} catch (e) { | |
console.error("Erro ao parsear JSON do buffer final:", jsonData.substring(0, 200)); | |
} | |
} | |
} | |
} | |
} catch (error) { | |
debugLog(`ERRO NO FETCH: ${error.message}`); | |
showError('A conexão com o servidor falhou.'); | |
loader.style.display = 'none'; | |
console.error("Fetch Error:", error); | |
} | |
} | |
// --- Lógica do Botão de Merge --- | |
mergeBtn.addEventListener('click', async function() { | |
debugLog("=== MERGE BUTTON CLICADO ==="); | |
currentProcessingType = 'merge'; | |
// Melhoria 05/08/2025 - validação antes de chamar o servidor | |
const filled = ['openai-output','sonnet-output','gemini-output'] | |
.map(id => rawTexts[id]) | |
.filter(text => text && text.trim().length > 0) | |
.length; | |
if (filled < 2) { | |
showError('Para processar o merge, deve haver pelo menos dois textos gerados.'); | |
return; | |
} | |
loaderMessage.textContent = 'Processando o merge dos textos...'; | |
progressBar.style.width = '0%'; | |
loader.style.display = 'flex'; | |
cancelBtn.disabled = false; | |
cancelBtn.textContent = 'Cancelar Processamento'; | |
this.style.display = 'none'; | |
const payload = { | |
solicitacao_usuario: originalUserQuery, | |
openai_text: rawTexts['openai-output'] || '', | |
sonnet_text: rawTexts['sonnet-output'] || '', | |
gemini_text: rawTexts['gemini-output'] || '', | |
}; | |
debugLog(`Payload do merge preparado com textos de tamanho: G=${payload.openai_text.length}, S=${payload.sonnet_text.length}, G=${payload.gemini_text.length}`); | |
try { | |
const response = await fetch('/merge', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify(payload) | |
}); | |
if (!response.ok || !response.body) throw new Error(`Erro na resposta do servidor: ${response.statusText}`); | |
debugLog("=== MERGE FETCH REALIZADO COM SUCESSO, INICIANDO STREAM ==="); | |
const reader = response.body.getReader(); | |
const decoder = new TextDecoder(); | |
let buffer = ''; | |
while (true) { | |
const { done, value } = await reader.read(); | |
if (done) { | |
debugLog("=== MERGE STREAM FINALIZADO ==="); | |
break; | |
} | |
const chunk = decoder.decode(value, { stream: true }); | |
buffer += chunk; | |
debugLog(`Merge chunk recebido: ${chunk.length} chars`); | |
let lines = buffer.split('\n\n'); | |
buffer = lines.pop(); | |
lines.forEach(line => { | |
if (line.startsWith('data: ')) { | |
const jsonData = line.substring(6).trim(); | |
if (jsonData) { | |
debugLog(`Merge JSON recebido: ${jsonData.substring(0, 200)}...`); | |
try { | |
const data = JSON.parse(jsonData); | |
debugLog(`Merge dados processados: ${JSON.stringify({progress: data.progress, message: data.message, hasFinalResult: !!data.final_result, error: data.error})}`); | |
processStreamData(data, true); | |
} catch (e) { | |
console.error("Erro ao parsear JSON do merge:", jsonData.substring(0, 500)); | |
console.error("Erro:", e); | |
} | |
} | |
} | |
}); | |
} | |
if (buffer.trim()) { | |
debugLog(`Merge buffer final: ${buffer.substring(0, 200)}...`); | |
if (buffer.startsWith('data: ')) { | |
const jsonData = buffer.substring(6).trim(); | |
if (jsonData) { | |
try { | |
const data = JSON.parse(jsonData); | |
debugLog("Processando buffer final do merge"); | |
processStreamData(data, true); | |
} catch (e) { | |
console.error("Erro ao parsear JSON do merge buffer final:", jsonData.substring(0, 500)); | |
console.error("Erro:", e); | |
} | |
} | |
} | |
} | |
} catch (error) { | |
debugLog(`ERRO NO MERGE: ${error.message}`); | |
showError("A conexão falhou ao tentar processar o merge."); | |
loader.style.display = 'none'; | |
} | |
}); | |
// --- Função de Processamento de Stream --- | |
function processStreamData(data, isMerge) { | |
debugLog(`=== PROCESSANDO STREAM DATA ===`); | |
debugLog(`Progress: ${data.progress}, Message: ${data.message}`); | |
debugLog(`Has partial_result: ${!!data.partial_result}`); | |
debugLog(`Has final_result: ${!!data.final_result}`); | |
debugLog(`Has error: ${!!data.error}`); | |
debugLog(`Is merge: ${isMerge}`); | |
if (data.error) { | |
debugLog(`Erro recebido: ${data.error}`); | |
showError(data.error); | |
loader.style.display = 'none'; | |
return; | |
} | |
if (data.progress !== undefined) { | |
loaderMessage.textContent = data.message || 'Processando...'; | |
progressBar.style.width = data.progress + '%'; | |
debugLog(`Progress atualizado: ${data.progress}%`); | |
} | |
const processContent = (targetId, content) => { | |
debugLog(`Processando conteúdo para: ${targetId}`); | |
debugLog(`Tamanho do conteúdo: ${content.length} chars`); | |
const targetBox = document.getElementById(targetId); | |
if (!targetBox) { | |
debugLog(`ERRO: Box não encontrado: ${targetId}`); | |
return; | |
} | |
rawTexts[targetId] = content; | |
targetBox.innerText = content; | |
debugLog(`Conteúdo armazenado e exibido para: ${targetId}`); | |
if (targetId === 'final-output') { | |
const finalTitle = document.getElementById('final-result-title'); | |
finalTitle.textContent = `Texto Final (${content.length} Caracteres)`; | |
finalResultContainer.style.display = 'block'; | |
debugLog("Final result container exibido"); | |
setTimeout(() => { | |
finalResultContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
debugLog("Scroll automático para texto final executado"); | |
}, 500); | |
} else { | |
const column = targetBox.closest('.result-column'); | |
const header = column.querySelector('.column-header h2'); | |
const base = header.textContent.split(' (')[0]; | |
header.textContent = `${base} (${content.length} Caracteres)`; | |
resultsContainer.style.display = 'flex'; | |
debugLog("Results container exibido"); | |
} | |
}; | |
if (isMerge && data.final_result) { | |
debugLog("Processando final result do merge"); | |
processContent('final-output', data.final_result.content); | |
} else if (data.partial_result) { | |
debugLog(`Processando partial result para: ${data.partial_result.id}`); | |
processContent(data.partial_result.id, data.partial_result.content); | |
} | |
if (data.done) { | |
debugLog("=== PROCESSAMENTO CONCLUÍDO ==="); | |
setTimeout(() => { | |
loader.style.display = 'none'; | |
if (data.mode === 'atomic' && !isMerge) { | |
mergeBtn.style.display = 'block'; | |
debugLog("Merge button exibido para modo atomic"); | |
} | |
}, 500); | |
} | |
} | |
// --- Funções de Utilitários --- | |
function showError(message) { | |
debugLog(`Exibindo erro: ${message}`); | |
errorContainer.innerHTML = `<div class="error-box"><strong>Erro:</strong> ${message}<span class="close-btn-error" onclick="this.parentElement.style.display='none';" title="Fechar">×</span></div>`; | |
} | |
function copyToClipboard(elementId) { | |
debugLog(`Copiando conteúdo de: ${elementId}`); | |
const textToCopy = rawTexts[elementId]; | |
if (textToCopy !== undefined) { | |
navigator.clipboard.writeText(textToCopy).then(() => { | |
// Feedback visual melhorado para mobile | |
const button = event.target; | |
const originalText = button.textContent; | |
button.textContent = 'Copiado!'; | |
button.style.backgroundColor = '#28a745'; | |
setTimeout(() => { | |
button.textContent = originalText; | |
button.style.backgroundColor = ''; | |
}, 2000); | |
debugLog('Texto copiado com sucesso'); | |
}); | |
} else { | |
debugLog(`ERRO: Texto não encontrado para ${elementId}`); | |
alert('Nenhum texto para copiar.'); | |
} | |
} | |
async function convertToMarkdown(elementId) { | |
debugLog(`Convertendo markdown para: ${elementId}`); | |
const button = event.target; | |
button.disabled = true; | |
button.innerText = 'Convertendo...'; | |
const rawText = rawTexts[elementId]; | |
if (rawText === undefined) { | |
debugLog(`ERRO: Texto bruto não encontrado para ${elementId}`); | |
showError('Não há texto para converter.'); | |
button.innerText = 'Converter para MD'; | |
button.disabled = false; | |
return; | |
} | |
try { | |
const response = await fetch('/convert', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ text: rawText }) | |
}); | |
if (!response.ok) throw new Error('Falha na conversão'); | |
const data = await response.json(); | |
const targetBox = document.getElementById(elementId); | |
targetBox.innerHTML = data.html; | |
button.innerText = 'Convertido'; | |
debugLog(`Markdown convertido com sucesso para ${elementId}`); | |
} catch (error) { | |
showError('Não foi possível converter o texto.'); | |
console.error('Conversion error:', error); | |
debugLog(`Erro na conversão: ${error.message}`); | |
button.innerText = 'Converter para MD'; | |
button.disabled = false; | |
} | |
} | |
// Detectar orientação e ajustar layout | |
function handleOrientationChange() { | |
setTimeout(() => { | |
// Forçar reflow para ajustar elementos após mudança de orientação | |
document.body.style.display = 'none'; | |
document.body.offsetHeight; // Trigger reflow | |
document.body.style.display = ''; | |
}, 100); | |
} | |
window.addEventListener('orientationchange', handleOrientationChange); | |
window.addEventListener('resize', handleOrientationChange); | |
</script> | |
</body> | |
</html> | |