victorafarias commited on
Commit
5b91c75
·
1 Parent(s): 7f104b6

Implementação da conversão posterior do texto para md

Browse files
Files changed (2) hide show
  1. app.py +33 -39
  2. templates/index.html +152 -112
app.py CHANGED
@@ -1,6 +1,6 @@
1
  # app.py
2
 
3
- from flask import Flask, render_template, request, Response
4
  import json
5
  import time
6
  import os
@@ -36,8 +36,7 @@ app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
36
  # Instancia o conversor de Markdown
37
  md = MarkdownIt()
38
 
39
- # [ADICIONADO] Função para renderização com fallback: tenta MarkdownIt, depois markdown2
40
-
41
  def render_markdown_cascata(texto: str) -> str:
42
  try:
43
  html_1 = md.render(texto)
@@ -47,7 +46,7 @@ def render_markdown_cascata(texto: str) -> str:
47
  print(f"MarkdownIt falhou: {e}")
48
 
49
  try:
50
- html_2 = markdown2_render(texto)
51
  if not is_html_empty(html_2):
52
  return html_2
53
  except Exception as e:
@@ -76,6 +75,18 @@ def index():
76
  """Renderiza a página inicial da aplicação."""
77
  return render_template('index.html')
78
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  @app.route('/process', methods=['POST'])
80
  def process():
81
  """Processa a solicitação do usuário nos modos Hierárquico ou Atômico."""
@@ -98,12 +109,12 @@ def process():
98
  solicitacao_usuario = form_data.get('solicitacao', '')
99
 
100
  if current_mode == 'test':
101
- mock_text = form_data.get('mock_text', 'Este é um texto de simulação.')
102
- mock_html = md.render(mock_text)
103
- yield f"data: {json.dumps({'progress': 100, 'message': 'Simulação concluída!', 'partial_result': {'id': 'grok-output', 'content': mock_html}, 'done': True, 'mode': 'atomic' if processing_mode == 'atomic' else 'hierarchical'})}\n\n"
104
  if processing_mode == 'atomic':
105
- yield f"data: {json.dumps({'partial_result': {'id': 'sonnet-output', 'content': mock_html}})}\n\n"
106
- yield f"data: {json.dumps({'partial_result': {'id': 'gemini-output', 'content': mock_html}})}\n\n"
107
  else:
108
  if not solicitacao_usuario:
109
  yield f"data: {json.dumps({'error': 'Solicitação não fornecida.'})}\n\n"
@@ -160,26 +171,18 @@ def process():
160
 
161
  yield f"data: {json.dumps({'progress': 80, 'message': 'Todos os modelos responderam. Formatando saídas...'})}\n\n"
162
 
 
163
  grok_text = results.get('grok', '')
164
  print(f"--- Resposta Bruta do GROK (Atômico) ---\n{grok_text}\n--------------------------------------")
165
- grok_html = render_markdown_cascata(grok_text)
166
- if is_html_empty(grok_html):
167
- grok_html = f"<pre>{escape(grok_text)}</pre>"
168
- yield f"data: {json.dumps({'partial_result': {'id': 'grok-output', 'content': grok_html}})}\n\n"
169
 
170
  sonnet_text = results.get('sonnet', '')
171
  print(f"--- Resposta Bruta do Sonnet (Atômico) ---\n{sonnet_text}\n----------------------------------------")
172
- sonnet_html = render_markdown_cascata(sonnet_text)
173
- if is_html_empty(sonnet_html):
174
- sonnet_html = f"<pre>{escape(sonnet_text)}</pre>"
175
- yield f"data: {json.dumps({'partial_result': {'id': 'sonnet-output', 'content': sonnet_html}})}\n\n"
176
 
177
  gemini_text = results.get('gemini', '')
178
  print(f"--- Resposta Bruta do Gemini (Atômico) ---\n{gemini_text}\n----------------------------------------")
179
- gemini_html = render_markdown_cascata(gemini_text)
180
- if is_html_empty(gemini_html):
181
- gemini_html = f"<pre>{escape(gemini_text)}</pre>"
182
- yield f"data: {json.dumps({'partial_result': {'id': 'gemini-output', 'content': gemini_html}})}\n\n"
183
 
184
  yield f"data: {json.dumps({'progress': 100, 'message': 'Processamento Atômico concluído!', 'done': True, 'mode': 'atomic'})}\n\n"
185
 
@@ -195,10 +198,8 @@ def process():
195
  return
196
 
197
  print(f"--- Resposta Bruta do GROK (Hierárquico) ---\n{resposta_grok}\n------------------------------------------")
198
- grok_html = render_markdown_cascata(resposta_grok)
199
- if is_html_empty(grok_html):
200
- grok_html = f"<pre>{escape(resposta_grok)}</pre>"
201
- yield f"data: {json.dumps({'progress': 33, 'message': 'Claude Sonnet está processando...', 'partial_result': {'id': 'grok-output', 'content': grok_html}})}\n\n"
202
 
203
  prompt_sonnet = PromptTemplate(template=PROMPT_HIERARQUICO_SONNET, input_variables=["solicitacao_usuario", "texto_para_analise"])
204
  claude_with_max_tokens = claude_llm.bind(max_tokens=20000)
@@ -210,10 +211,8 @@ def process():
210
  return
211
 
212
  print(f"--- Resposta Bruta do Sonnet (Hierárquico) ---\n{resposta_sonnet}\n--------------------------------------------")
213
- sonnet_html = render_markdown_cascata(resposta_sonnet)
214
- if is_html_empty(sonnet_html):
215
- sonnet_html = f"<pre>{escape(resposta_sonnet)}</pre>"
216
- yield f"data: {json.dumps({'progress': 66, 'message': 'Gemini está processando...', 'partial_result': {'id': 'sonnet-output', 'content': sonnet_html}})}\n\n"
217
 
218
  prompt_gemini = PromptTemplate(template=PROMPT_HIERARQUICO_GEMINI, input_variables=["solicitacao_usuario", "texto_para_analise"])
219
  chain_gemini = prompt_gemini | gemini_llm | output_parser
@@ -224,10 +223,8 @@ def process():
224
  return
225
 
226
  print(f"--- Resposta Bruta do Gemini (Hierárquico) ---\n{resposta_gemini}\n--------------------------------------------")
227
- gemini_html = render_markdown_cascata(resposta_gemini)
228
- if is_html_empty(gemini_html):
229
- gemini_html = f"<pre>{escape(gemini_html)}</pre>"
230
- yield f"data: {json.dumps({'progress': 100, 'message': 'Processamento concluído!', 'partial_result': {'id': 'gemini-output', 'content': gemini_html}, 'done': True, 'mode': 'hierarchical'})}\n\n"
231
 
232
  except Exception as e:
233
  print(f"Ocorreu um erro durante o processamento: {e}")
@@ -267,11 +264,8 @@ def merge():
267
  print(f"--- Resposta Bruta do Merge (GROK) ---\n{resposta_merge}\n------------------------------------")
268
  word_count = len(resposta_merge.split())
269
 
270
- merge_html = render_markdown_cascata(resposta_merge)
271
- if is_html_empty(merge_html):
272
- merge_html = f"<pre>{escape(resposta_merge)}</pre>"
273
-
274
- yield f"data: {json.dumps({'progress': 100, 'message': 'Merge concluído!', 'final_result': {'content': merge_html, 'word_count': word_count}, 'done': True})}\n\n"
275
 
276
  except Exception as e:
277
  print(f"Erro no processo de merge: {e}")
@@ -280,4 +274,4 @@ def merge():
280
  return Response(generate_merge_stream(), mimetype='text/event-stream')
281
 
282
  if __name__ == '__main__':
283
- app.run(debug=True)
 
1
  # app.py
2
 
3
+ from flask import Flask, render_template, request, Response, jsonify
4
  import json
5
  import time
6
  import os
 
36
  # Instancia o conversor de Markdown
37
  md = MarkdownIt()
38
 
39
+ # Função para renderização com fallback: tenta MarkdownIt, depois markdown2
 
40
  def render_markdown_cascata(texto: str) -> str:
41
  try:
42
  html_1 = md.render(texto)
 
46
  print(f"MarkdownIt falhou: {e}")
47
 
48
  try:
49
+ html_2 = markdown2_render(texto, extras=["fenced-code-blocks", "tables"])
50
  if not is_html_empty(html_2):
51
  return html_2
52
  except Exception as e:
 
75
  """Renderiza a página inicial da aplicação."""
76
  return render_template('index.html')
77
 
78
+ # ROTA ATUALIZADA: Para converter texto em Markdown sob demanda
79
+ @app.route('/convert', methods=['POST'])
80
+ def convert():
81
+ data = request.get_json()
82
+ if not data or 'text' not in data:
83
+ return jsonify({'error': 'Nenhum texto fornecido'}), 400
84
+
85
+ text_to_convert = data['text']
86
+ # USA A FUNÇÃO DE CASCATA PARA MAIOR ROBUSTEZ
87
+ converted_html = render_markdown_cascata(text_to_convert)
88
+ return jsonify({'html': converted_html})
89
+
90
  @app.route('/process', methods=['POST'])
91
  def process():
92
  """Processa a solicitação do usuário nos modos Hierárquico ou Atômico."""
 
109
  solicitacao_usuario = form_data.get('solicitacao', '')
110
 
111
  if current_mode == 'test':
112
+ mock_text = form_data.get('mock_text', 'Este é um **texto** de `simulação`.')
113
+ # MUDANÇA: Envia o texto bruto na simulação
114
+ yield f"data: {json.dumps({'progress': 100, 'message': 'Simulação concluída!', 'partial_result': {'id': 'grok-output', 'content': mock_text}, 'done': True, 'mode': 'atomic' if processing_mode == 'atomic' else 'hierarchical'})}\n\n"
115
  if processing_mode == 'atomic':
116
+ yield f"data: {json.dumps({'partial_result': {'id': 'sonnet-output', 'content': mock_text}})}\n\n"
117
+ yield f"data: {json.dumps({'partial_result': {'id': 'gemini-output', 'content': mock_text}})}\n\n"
118
  else:
119
  if not solicitacao_usuario:
120
  yield f"data: {json.dumps({'error': 'Solicitação não fornecida.'})}\n\n"
 
171
 
172
  yield f"data: {json.dumps({'progress': 80, 'message': 'Todos os modelos responderam. Formatando saídas...'})}\n\n"
173
 
174
+ # MUDANÇA: Envia o texto bruto para cada modelo
175
  grok_text = results.get('grok', '')
176
  print(f"--- Resposta Bruta do GROK (Atômico) ---\n{grok_text}\n--------------------------------------")
177
+ yield f"data: {json.dumps({'partial_result': {'id': 'grok-output', 'content': grok_text}})}\n\n"
 
 
 
178
 
179
  sonnet_text = results.get('sonnet', '')
180
  print(f"--- Resposta Bruta do Sonnet (Atômico) ---\n{sonnet_text}\n----------------------------------------")
181
+ yield f"data: {json.dumps({'partial_result': {'id': 'sonnet-output', 'content': sonnet_text}})}\n\n"
 
 
 
182
 
183
  gemini_text = results.get('gemini', '')
184
  print(f"--- Resposta Bruta do Gemini (Atômico) ---\n{gemini_text}\n----------------------------------------")
185
+ yield f"data: {json.dumps({'partial_result': {'id': 'gemini-output', 'content': gemini_text}})}\n\n"
 
 
 
186
 
187
  yield f"data: {json.dumps({'progress': 100, 'message': 'Processamento Atômico concluído!', 'done': True, 'mode': 'atomic'})}\n\n"
188
 
 
198
  return
199
 
200
  print(f"--- Resposta Bruta do GROK (Hierárquico) ---\n{resposta_grok}\n------------------------------------------")
201
+ # MUDANÇA: Envia o texto bruto em vez de HTML
202
+ yield f"data: {json.dumps({'progress': 33, 'message': 'Claude Sonnet está processando...', 'partial_result': {'id': 'grok-output', 'content': resposta_grok}})}\n\n"
 
 
203
 
204
  prompt_sonnet = PromptTemplate(template=PROMPT_HIERARQUICO_SONNET, input_variables=["solicitacao_usuario", "texto_para_analise"])
205
  claude_with_max_tokens = claude_llm.bind(max_tokens=20000)
 
211
  return
212
 
213
  print(f"--- Resposta Bruta do Sonnet (Hierárquico) ---\n{resposta_sonnet}\n--------------------------------------------")
214
+ # MUDANÇA: Envia o texto bruto em vez de HTML
215
+ yield f"data: {json.dumps({'progress': 66, 'message': 'Gemini está processando...', 'partial_result': {'id': 'sonnet-output', 'content': resposta_sonnet}})}\n\n"
 
 
216
 
217
  prompt_gemini = PromptTemplate(template=PROMPT_HIERARQUICO_GEMINI, input_variables=["solicitacao_usuario", "texto_para_analise"])
218
  chain_gemini = prompt_gemini | gemini_llm | output_parser
 
223
  return
224
 
225
  print(f"--- Resposta Bruta do Gemini (Hierárquico) ---\n{resposta_gemini}\n--------------------------------------------")
226
+ # MUDANÇA: Envia o texto bruto em vez de HTML
227
+ yield f"data: {json.dumps({'progress': 100, 'message': 'Processamento concluído!', 'partial_result': {'id': 'gemini-output', 'content': resposta_gemini}, 'done': True, 'mode': 'hierarchical'})}\n\n"
 
 
228
 
229
  except Exception as e:
230
  print(f"Ocorreu um erro durante o processamento: {e}")
 
264
  print(f"--- Resposta Bruta do Merge (GROK) ---\n{resposta_merge}\n------------------------------------")
265
  word_count = len(resposta_merge.split())
266
 
267
+ # MUDANÇA: Envia o texto bruto do merge em vez de HTML
268
+ yield f"data: {json.dumps({'progress': 100, 'message': 'Merge concluído!', 'final_result': {'content': resposta_merge, 'word_count': word_count}, 'done': True})}\n\n"
 
 
 
269
 
270
  except Exception as e:
271
  print(f"Erro no processo de merge: {e}")
 
274
  return Response(generate_merge_stream(), mimetype='text/event-stream')
275
 
276
  if __name__ == '__main__':
277
+ app.run(debug=True)
templates/index.html CHANGED
@@ -5,6 +5,22 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Sistema Multi-Agente de IA</title>
7
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  </head>
9
  <body>
10
 
@@ -12,24 +28,19 @@
12
  <div class="loader-content">
13
  <div class="loader-spinner"></div>
14
  <p id="loader-message">Processando sua solicitação...</p>
15
- <div class="progress-bar-container"><div id="progress-bar" class="progress-bar"></div></div>
 
 
16
  </div>
17
  </div>
18
 
19
- <button id="merge-btn" class="floating-merge-btn" style="display: none;">Processar Merge</button>
20
-
21
  <div class="container">
22
  <div class="header-container">
23
  <div>
24
  <h1>Sistema Multi-Agente IA</h1>
25
- <p id="flow-description">GROK ➔ Claude Sonnet ➔ Gemini</p>
26
  </div>
27
  <div class="controls-container">
28
- <div class="mode-toggle" title="A versão 'Hierárquica' gerará um único no 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.">
29
- <span>Hierárquico</span>
30
- <label class="switch"><input type="checkbox" id="processing-mode-switch"><span class="slider round"></span></label>
31
- <span>Atômico</span>
32
- </div>
33
  <div class="mode-toggle">
34
  <span>Modo Real</span>
35
  <label class="switch"><input type="checkbox" id="mode-switch"><span class="slider round"></span></label>
@@ -49,30 +60,55 @@
49
  <button type="submit">Processar com IA</button>
50
  </form>
51
  </div>
 
52
  <div id="mock-form-container" style="display: none;">
53
  <form id="request-form-mock">
54
  <label for="mock_text">Cole o texto de simulação aqui:</label>
55
- <textarea name="mock_text" id="mock_text" rows="10" required></textarea>
 
 
 
 
 
56
  <button type="submit">Simular Resposta</button>
57
  </form>
58
  </div>
59
 
60
  <div id="results-container" class="results-container" style="display: none;">
61
- <div class="result-column"><div class="column-header"><h2>GROK</h2><button class="copy-btn" onclick="copyToClipboard('grok-output')">Copiar</button></div><div class="output-box" id="grok-output"></div></div>
62
- <div class="result-column"><div class="column-header"><h2>Claude Sonnet</h2><button class="copy-btn" onclick="copyToClipboard('sonnet-output')">Copiar</button></div><div class="output-box" id="sonnet-output"></div></div>
63
- <div class="result-column"><div class="column-header"><h2>Gemini</h2><button class="copy-btn" onclick="copyToClipboard('gemini-output')">Copiar</button></div><div class="output-box" id="gemini-output"></div></div>
64
- </div>
65
-
66
- <!-- Container do Resultado Final com ID no título -->
67
- <div id="final-result-container" style="display: none;">
68
- <h2 id="final-result-title">Texto Final</h2>
69
- <div class="output-box" id="final-output"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  </div>
71
  </div>
72
 
73
  <script>
74
- // --- Variáveis Globais e Lógica de UI (sem alterações) ---
75
- const processingModeSwitch = document.getElementById('processing-mode-switch');
76
  const modeSwitch = document.getElementById('mode-switch');
77
  const realContainer = document.getElementById('real-form-container');
78
  const mockContainer = document.getElementById('mock-form-container');
@@ -83,39 +119,47 @@
83
  const errorContainer = document.getElementById('error-box-container');
84
  const textarea = document.getElementById('solicitacao_usuario');
85
  const fileList = document.getElementById('file-list');
86
- const mergeBtn = document.getElementById('merge-btn');
87
- const finalResultContainer = document.getElementById('final-result-container');
88
- const finalOutput = document.getElementById('final-output');
89
  let attachedFiles = [];
90
- let originalUserQuery = "";
 
 
91
 
92
  modeSwitch.addEventListener('change', function() {
93
  realContainer.style.display = this.checked ? 'none' : 'block';
94
  mockContainer.style.display = this.checked ? 'block' : 'none';
95
  });
96
- processingModeSwitch.addEventListener('change', function() {
97
- document.getElementById('flow-description').textContent = this.checked ? "GROK | Claude Sonnet | Gemini (Paralelo)" : "GROK ➔ Claude Sonnet ➔ Gemini";
98
- });
99
-
100
- // --- Lógica de Upload (sem alterações) ---
101
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
102
  textarea.addEventListener(eventName, preventDefaults, false);
103
  document.body.addEventListener(eventName, preventDefaults, false);
104
  });
105
- ['dragenter', 'dragover'].forEach(eventName => textarea.addEventListener(eventName, () => textarea.classList.add('drag-over'), false));
106
- ['dragleave', 'drop'].forEach(eventName => textarea.addEventListener(eventName, () => textarea.classList.remove('drag-over'), false));
 
 
 
 
107
  textarea.addEventListener('drop', handleDrop, false);
 
108
  function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
109
  function handleDrop(e) { handleFiles(e.dataTransfer.files); }
 
110
  function handleFiles(files) {
111
  [...files].forEach(file => {
112
  const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'];
113
- if (!allowedTypes.includes(file.type)) { showError(`Formato não suportado: ${file.name}`); return; }
114
- if (file.size > 100 * 1024 * 1024) { showError(`Arquivo muito grande: ${file.name}`); return; }
 
 
 
 
 
 
115
  attachedFiles.push(file);
116
  });
117
  updateFileList();
118
  }
 
119
  function updateFileList() {
120
  fileList.innerHTML = '';
121
  document.getElementById('file-list-container').style.display = attachedFiles.length > 0 ? 'block' : 'none';
@@ -125,116 +169,84 @@
125
  const removeBtn = document.createElement('span');
126
  removeBtn.textContent = '×';
127
  removeBtn.className = 'remove-file-btn';
128
- removeBtn.onclick = () => { attachedFiles.splice(index, 1); updateFileList(); };
 
 
 
129
  li.appendChild(removeBtn);
130
  fileList.appendChild(li);
131
  });
132
  }
133
 
134
- // --- Lógica de Submissão Principal (sem alterações) ---
135
  document.getElementById('request-form-real').addEventListener('submit', handleFormSubmit);
136
  document.getElementById('request-form-mock').addEventListener('submit', handleFormSubmit);
137
 
138
  async function handleFormSubmit(event) {
139
  event.preventDefault();
 
140
  errorContainer.innerHTML = '';
141
  resultsContainer.style.display = 'none';
142
- finalResultContainer.style.display = 'none';
143
- mergeBtn.style.display = 'none';
144
  document.querySelectorAll('.output-box').forEach(box => box.innerHTML = '');
 
 
145
  loaderMessage.textContent = 'Iniciando conexão...';
146
  progressBar.style.width = '0%';
147
  loader.style.display = 'flex';
 
148
  const formData = new FormData();
149
- formData.append('processing_mode', processingModeSwitch.checked ? 'atomic' : 'hierarchical');
150
  if (modeSwitch.checked) {
151
  formData.append('mode', 'test');
152
  formData.append('mock_text', document.getElementById('mock_text').value);
153
- originalUserQuery = "Simulação de teste.";
154
  } else {
155
  formData.append('mode', 'real');
156
- originalUserQuery = document.getElementById('solicitacao_usuario').value;
157
- formData.append('solicitacao', originalUserQuery);
158
- attachedFiles.forEach(file => { formData.append('files', file); });
159
- }
160
- try {
161
- const response = await fetch('/process', { method: 'POST', body: formData });
162
- if (!response.ok || !response.body) throw new Error(`Erro na resposta do servidor: ${response.statusText}`);
163
- const reader = response.body.getReader();
164
- const decoder = new TextDecoder();
165
- while (true) {
166
- const { done, value } = await reader.read();
167
- if (done) break;
168
- const chunk = decoder.decode(value, { stream: true });
169
- const lines = chunk.split('\n\n');
170
- lines.forEach(line => {
171
- if (line.startsWith('data: ')) {
172
- const jsonData = line.substring(6);
173
- if (jsonData.trim()) {
174
- try {
175
- const data = JSON.parse(jsonData);
176
- processStreamData(data, false);
177
- } catch (e) { console.error("Erro ao parsear JSON:", jsonData); }
178
- }
179
- }
180
- });
181
- }
182
- } catch (error) {
183
- showError('A conexão com o servidor falhou.');
184
- loader.style.display = 'none';
185
- console.error("Fetch Error:", error);
186
  }
187
- }
188
-
189
- // --- Lógica do Botão de Merge com Streaming ---
190
- mergeBtn.addEventListener('click', async function() {
191
- loaderMessage.textContent = 'Processando o merge dos textos...';
192
- progressBar.style.width = '0%';
193
- loader.style.display = 'flex';
194
- this.style.display = 'none';
195
-
196
- const payload = {
197
- solicitacao_usuario: originalUserQuery,
198
- grok_text: document.getElementById('grok-output').innerText,
199
- sonnet_text: document.getElementById('sonnet-output').innerText,
200
- gemini_text: document.getElementById('gemini-output').innerText,
201
- };
202
 
203
  try {
204
- const response = await fetch('/merge', {
205
  method: 'POST',
206
- headers: { 'Content-Type': 'application/json' },
207
- body: JSON.stringify(payload)
208
  });
209
- if (!response.ok || !response.body) throw new Error(`Erro na resposta do servidor: ${response.statusText}`);
210
-
 
 
 
211
  const reader = response.body.getReader();
212
  const decoder = new TextDecoder();
 
213
  while (true) {
214
  const { done, value } = await reader.read();
215
  if (done) break;
 
216
  const chunk = decoder.decode(value, { stream: true });
217
  const lines = chunk.split('\n\n');
 
218
  lines.forEach(line => {
219
  if (line.startsWith('data: ')) {
220
  const jsonData = line.substring(6);
221
  if (jsonData.trim()) {
222
  try {
223
  const data = JSON.parse(jsonData);
224
- processStreamData(data, true); // true = é merge
225
- } catch (e) { console.error("Erro ao parsear JSON do merge:", jsonData); }
 
 
226
  }
227
  }
228
  });
229
  }
230
  } catch (error) {
231
- showError("A conexão falhou ao tentar processar o merge.");
232
  loader.style.display = 'none';
 
233
  }
234
- });
235
 
236
- // --- Função de Processamento de Stream ATUALIZADA ---
237
- function processStreamData(data, isMerge) {
238
  if (data.error) {
239
  showError(data.error);
240
  loader.style.display = 'none';
@@ -244,26 +256,19 @@
244
  loaderMessage.textContent = data.message;
245
  progressBar.style.width = data.progress + '%';
246
 
247
- if (isMerge && data.final_result) {
248
- const finalTitle = document.getElementById('final-result-title');
249
- finalOutput.innerHTML = data.final_result.content;
250
- // ATUALIZAÇÃO: Modifica o título com a contagem de palavras
251
- if (data.final_result.word_count) {
252
- finalTitle.textContent = `Texto Final (${data.final_result.word_count} palavras)`;
253
- }
254
- finalResultContainer.style.display = 'block';
255
- } else if (data.partial_result) {
256
  resultsContainer.style.display = 'flex';
257
  const targetBox = document.getElementById(data.partial_result.id);
258
- if (targetBox) targetBox.innerHTML = data.partial_result.content;
 
 
 
 
259
  }
260
 
261
  if (data.done) {
262
  setTimeout(() => {
263
  loader.style.display = 'none';
264
- if (data.mode === 'atomic' && !isMerge) {
265
- mergeBtn.style.display = 'block';
266
- }
267
  }, 1000);
268
  }
269
  }
@@ -273,9 +278,44 @@
273
  }
274
 
275
  function copyToClipboard(elementId) {
276
- const element = document.getElementById(elementId);
277
- navigator.clipboard.writeText(element.innerText).then(() => { alert('Texto copiado!'); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  }
279
  </script>
280
  </body>
281
- </html>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Sistema Multi-Agente de IA</title>
7
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
8
+ <style>
9
+ /* Adicionando um estilo simples para o novo botão */
10
+ .convert-btn {
11
+ padding: 5px 10px;
12
+ font-size: 12px;
13
+ background-color: #17a2b8; /* Uma cor diferente */
14
+ color: white;
15
+ border: none;
16
+ border-radius: 4px;
17
+ cursor: pointer;
18
+ margin-left: 5px; /* Espaço entre os botões */
19
+ }
20
+ .convert-btn:hover {
21
+ background-color: #138496;
22
+ }
23
+ </style>
24
  </head>
25
  <body>
26
 
 
28
  <div class="loader-content">
29
  <div class="loader-spinner"></div>
30
  <p id="loader-message">Processando sua solicitação...</p>
31
+ <div class="progress-bar-container">
32
+ <div id="progress-bar" class="progress-bar"></div>
33
+ </div>
34
  </div>
35
  </div>
36
 
 
 
37
  <div class="container">
38
  <div class="header-container">
39
  <div>
40
  <h1>Sistema Multi-Agente IA</h1>
41
+ <p>GROK ➔ Claude Sonnet ➔ Gemini</p>
42
  </div>
43
  <div class="controls-container">
 
 
 
 
 
44
  <div class="mode-toggle">
45
  <span>Modo Real</span>
46
  <label class="switch"><input type="checkbox" id="mode-switch"><span class="slider round"></span></label>
 
60
  <button type="submit">Processar com IA</button>
61
  </form>
62
  </div>
63
+
64
  <div id="mock-form-container" style="display: none;">
65
  <form id="request-form-mock">
66
  <label for="mock_text">Cole o texto de simulação aqui:</label>
67
+ <textarea name="mock_text" id="mock_text" rows="10" required>Este é um **texto** de `simulação`.
68
+
69
+ - Item 1
70
+ - Item 2
71
+
72
+ Use o botão "Converter para MD" para ver o resultado formatado.</textarea>
73
  <button type="submit">Simular Resposta</button>
74
  </form>
75
  </div>
76
 
77
  <div id="results-container" class="results-container" style="display: none;">
78
+ <div class="result-column">
79
+ <div class="column-header">
80
+ <h2>GROK</h2>
81
+ <div>
82
+ <button class="copy-btn" onclick="copyToClipboard('grok-output')">Copiar</button>
83
+ <button class="convert-btn" onclick="convertToMarkdown('grok-output')">Converter para MD</button>
84
+ </div>
85
+ </div>
86
+ <div class="output-box" id="grok-output"></div>
87
+ </div>
88
+ <div class="result-column">
89
+ <div class="column-header">
90
+ <h2>Claude Sonnet</h2>
91
+ <div>
92
+ <button class="copy-btn" onclick="copyToClipboard('sonnet-output')">Copiar</button>
93
+ <button class="convert-btn" onclick="convertToMarkdown('sonnet-output')">Converter para MD</button>
94
+ </div>
95
+ </div>
96
+ <div class="output-box" id="sonnet-output"></div>
97
+ </div>
98
+ <div class="result-column">
99
+ <div class="column-header">
100
+ <h2>Gemini</h2>
101
+ <div>
102
+ <button class="copy-btn" onclick="copyToClipboard('gemini-output')">Copiar</button>
103
+ <button class="convert-btn" onclick="convertToMarkdown('gemini-output')">Converter para MD</button>
104
+ </div>
105
+ </div>
106
+ <div class="output-box" id="gemini-output"></div>
107
+ </div>
108
  </div>
109
  </div>
110
 
111
  <script>
 
 
112
  const modeSwitch = document.getElementById('mode-switch');
113
  const realContainer = document.getElementById('real-form-container');
114
  const mockContainer = document.getElementById('mock-form-container');
 
119
  const errorContainer = document.getElementById('error-box-container');
120
  const textarea = document.getElementById('solicitacao_usuario');
121
  const fileList = document.getElementById('file-list');
 
 
 
122
  let attachedFiles = [];
123
+
124
+ // Armazenar o texto bruto original para reconversão ou cópia
125
+ let rawTexts = {};
126
 
127
  modeSwitch.addEventListener('change', function() {
128
  realContainer.style.display = this.checked ? 'none' : 'block';
129
  mockContainer.style.display = this.checked ? 'block' : 'none';
130
  });
131
+
 
 
 
 
132
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
133
  textarea.addEventListener(eventName, preventDefaults, false);
134
  document.body.addEventListener(eventName, preventDefaults, false);
135
  });
136
+ ['dragenter', 'dragover'].forEach(eventName => {
137
+ textarea.addEventListener(eventName, () => textarea.classList.add('drag-over'), false);
138
+ });
139
+ ['dragleave', 'drop'].forEach(eventName => {
140
+ textarea.addEventListener(eventName, () => textarea.classList.remove('drag-over'), false);
141
+ });
142
  textarea.addEventListener('drop', handleDrop, false);
143
+
144
  function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
145
  function handleDrop(e) { handleFiles(e.dataTransfer.files); }
146
+
147
  function handleFiles(files) {
148
  [...files].forEach(file => {
149
  const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'];
150
+ if (!allowedTypes.includes(file.type)) {
151
+ showError(`Formato de arquivo não suportado: ${file.name}`);
152
+ return;
153
+ }
154
+ if (file.size > 100 * 1024 * 1024) { // 100MB
155
+ showError(`Arquivo muito grande (max 100MB): ${file.name}`);
156
+ return;
157
+ }
158
  attachedFiles.push(file);
159
  });
160
  updateFileList();
161
  }
162
+
163
  function updateFileList() {
164
  fileList.innerHTML = '';
165
  document.getElementById('file-list-container').style.display = attachedFiles.length > 0 ? 'block' : 'none';
 
169
  const removeBtn = document.createElement('span');
170
  removeBtn.textContent = '×';
171
  removeBtn.className = 'remove-file-btn';
172
+ removeBtn.onclick = () => {
173
+ attachedFiles.splice(index, 1);
174
+ updateFileList();
175
+ };
176
  li.appendChild(removeBtn);
177
  fileList.appendChild(li);
178
  });
179
  }
180
 
 
181
  document.getElementById('request-form-real').addEventListener('submit', handleFormSubmit);
182
  document.getElementById('request-form-mock').addEventListener('submit', handleFormSubmit);
183
 
184
  async function handleFormSubmit(event) {
185
  event.preventDefault();
186
+
187
  errorContainer.innerHTML = '';
188
  resultsContainer.style.display = 'none';
 
 
189
  document.querySelectorAll('.output-box').forEach(box => box.innerHTML = '');
190
+ rawTexts = {}; // Limpa os textos brutos anteriores
191
+
192
  loaderMessage.textContent = 'Iniciando conexão...';
193
  progressBar.style.width = '0%';
194
  loader.style.display = 'flex';
195
+
196
  const formData = new FormData();
 
197
  if (modeSwitch.checked) {
198
  formData.append('mode', 'test');
199
  formData.append('mock_text', document.getElementById('mock_text').value);
 
200
  } else {
201
  formData.append('mode', 'real');
202
+ formData.append('solicitacao', document.getElementById('solicitacao_usuario').value);
203
+ attachedFiles.forEach(file => {
204
+ formData.append('files', file);
205
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
  try {
209
+ const response = await fetch('/process', {
210
  method: 'POST',
211
+ body: formData,
 
212
  });
213
+
214
+ if (!response.ok || !response.body) {
215
+ throw new Error(`Erro na resposta do servidor: ${response.statusText}`);
216
+ }
217
+
218
  const reader = response.body.getReader();
219
  const decoder = new TextDecoder();
220
+
221
  while (true) {
222
  const { done, value } = await reader.read();
223
  if (done) break;
224
+
225
  const chunk = decoder.decode(value, { stream: true });
226
  const lines = chunk.split('\n\n');
227
+
228
  lines.forEach(line => {
229
  if (line.startsWith('data: ')) {
230
  const jsonData = line.substring(6);
231
  if (jsonData.trim()) {
232
  try {
233
  const data = JSON.parse(jsonData);
234
+ processStreamData(data);
235
+ } catch (e) {
236
+ console.error("Erro ao parsear JSON do stream:", jsonData);
237
+ }
238
  }
239
  }
240
  });
241
  }
242
  } catch (error) {
243
+ showError('A conexão com o servidor falhou. Verifique o console para detalhes.');
244
  loader.style.display = 'none';
245
+ console.error("Fetch Error:", error);
246
  }
247
+ }
248
 
249
+ function processStreamData(data) {
 
250
  if (data.error) {
251
  showError(data.error);
252
  loader.style.display = 'none';
 
256
  loaderMessage.textContent = data.message;
257
  progressBar.style.width = data.progress + '%';
258
 
259
+ if (data.partial_result) {
 
 
 
 
 
 
 
 
260
  resultsContainer.style.display = 'flex';
261
  const targetBox = document.getElementById(data.partial_result.id);
262
+ if (targetBox) {
263
+ // MUDANÇA: Exibe como texto bruto e armazena
264
+ rawTexts[data.partial_result.id] = data.partial_result.content;
265
+ targetBox.innerText = data.partial_result.content; // Use innerText para exibir texto bruto
266
+ }
267
  }
268
 
269
  if (data.done) {
270
  setTimeout(() => {
271
  loader.style.display = 'none';
 
 
 
272
  }, 1000);
273
  }
274
  }
 
278
  }
279
 
280
  function copyToClipboard(elementId) {
281
+ // MUDANÇA: Sempre copia o texto bruto original
282
+ const textToCopy = rawTexts[elementId];
283
+ if (textToCopy) {
284
+ navigator.clipboard.writeText(textToCopy).then(() => {
285
+ alert('Texto copiado!');
286
+ });
287
+ }
288
+ }
289
+
290
+ // NOVA FUNÇÃO: Para converter o texto para Markdown
291
+ async function convertToMarkdown(elementId) {
292
+ const button = event.target;
293
+ button.disabled = true; // Desabilita o botão para evitar cliques duplos
294
+ button.innerText = 'Convertendo...';
295
+
296
+ const rawText = rawTexts[elementId];
297
+ if (!rawText) return;
298
+
299
+ try {
300
+ const response = await fetch('/convert', {
301
+ method: 'POST',
302
+ headers: { 'Content-Type': 'application/json' },
303
+ body: JSON.stringify({ text: rawText })
304
+ });
305
+
306
+ if (!response.ok) throw new Error('Falha na conversão');
307
+
308
+ const data = await response.json();
309
+ const targetBox = document.getElementById(elementId);
310
+ targetBox.innerHTML = data.html; // Insere o HTML convertido
311
+ } catch (error) {
312
+ showError('Não foi possível converter o texto.');
313
+ console.error('Conversion error:', error);
314
+ } finally {
315
+ // Reabilita o botão, mas muda o texto para indicar que foi convertido
316
+ button.innerText = 'Convertido';
317
+ }
318
  }
319
  </script>
320
  </body>
321
+ </html>