oraculo / templates /index.html
victorafarias's picture
Versão Estável 13082025
f3597fc
<!DOCTYPE html>
<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 !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* 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">&times;</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>