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">GROK ➔ 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> | |
| <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>GROK</h2> | |
| <div> | |
| <button class="copy-btn" onclick="copyToClipboard('grok-output')">Copiar</button> | |
| <button class="convert-btn" onclick="convertToMarkdown('grok-output')">Converter para MD</button> | |
| </div> | |
| </div> | |
| <div class="output-box" id="grok-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'); | |
| 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'; | |
| }); | |
| processingModeSwitch.addEventListener('change', function() { | |
| const isAtomic = this.checked; | |
| document.getElementById('flow-description').textContent = isAtomic ? | |
| "GROK | Claude Sonnet | Gemini (Paralelo)" : | |
| "GROK ➔ 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) { | |
| 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); | |
| 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'; | |
| 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, | |
| grok_text: rawTexts['grok-output'] || '', | |
| sonnet_text: rawTexts['sonnet-output'] || '', | |
| gemini_text: rawTexts['gemini-output'] || '', | |
| }; | |
| debugLog(`Payload do merge preparado com textos de tamanho: G=${payload.grok_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, wordCount = null) => { | |
| 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`; | |
| if (wordCount) { | |
| finalTitle.textContent += ` (${wordCount} palavras)`; | |
| } | |
| finalResultContainer.style.display = 'block'; | |
| debugLog("Final result container exibido"); | |
| // Scroll automático para o texto final | |
| setTimeout(() => { | |
| finalResultContainer.scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'start' | |
| }); | |
| debugLog("Scroll automático para texto final executado"); | |
| }, 500); | |
| } else { | |
| 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, data.final_result.word_count); | |
| } 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> | |