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; | |
| } | |
| #final-result-container { | |
| display: none; | |
| width: 100%; | |
| margin-top: 20px; | |
| } | |
| .results-container .result-column { | |
| flex: 1; | |
| min-width: 0; | |
| } | |
| /* Estilos para os campos de tamanho do texto */ | |
| .text-size-controls { | |
| display: flex; | |
| gap: 15px; | |
| margin: 10px 0; | |
| align-items: center; | |
| background-color: #f8f9fa; | |
| padding: 15px; | |
| border-radius: 5px; | |
| border: 1px solid #e9ecef; | |
| } | |
| .text-size-field { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 5px; | |
| } | |
| .text-size-field label { | |
| font-size: 12px; | |
| color: #6c757d; | |
| font-weight: bold; | |
| } | |
| .text-size-field input { | |
| width: 80px; | |
| padding: 5px 8px; | |
| border: 1px solid #ced4da; | |
| border-radius: 3px; | |
| font-size: 14px; | |
| } | |
| /* Estilo para o botão de cancelar */ | |
| .cancel-btn { | |
| background-color: #dc3545; | |
| color: white; | |
| border: none; | |
| padding: 8px 15px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| margin-top: 10px; | |
| font-size: 14px; | |
| } | |
| .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; | |
| } | |
| </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"> | |
| <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> | |
| <!-- NOVOS CAMPOS: Controle de tamanho do texto --> | |
| <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 style="font-size: 12px; color: #6c757d; max-width: 300px;"> | |
| <strong>Dica:</strong> Defina o tamanho desejado para os textos gerados pelas IAs. 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 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; // 'main' ou 'merge' | |
| // 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('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 (ATUALIZADA) --- | |
| 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"); | |
| // MELHORIA 4: 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(() => { | |
| alert('Texto copiado!'); | |
| 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; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |