victorafarias commited on
Commit
d23c0dc
·
1 Parent(s): f239a52

Correçõe na conversão posterior do texto para md

Browse files
Files changed (2) hide show
  1. app.py +89 -33
  2. templates/index.html +166 -79
app.py CHANGED
@@ -70,6 +70,21 @@ def is_html_empty(html: str) -> bool:
70
  # 4. Verifica se o texto restante (após remover espaços nas pontas) está de fato vazio
71
  return not normalized_space.strip()
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  @app.route('/')
74
  def index():
75
  """Renderiza a página inicial da aplicação."""
@@ -111,17 +126,22 @@ def process():
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"
 
121
  return
122
 
123
  try:
124
- yield f"data: {json.dumps({'progress': 0, 'message': 'Processando arquivos e extraindo contexto...'})}\n\n"
 
125
  rag_context = get_relevant_context(file_paths, solicitacao_usuario)
126
 
127
  output_parser = StrOutputParser()
@@ -152,7 +172,8 @@ def process():
152
  models = {'grok': grok_llm, 'sonnet': claude_atomic_llm, 'gemini': gemini_llm}
153
 
154
  prompt = PromptTemplate(template=PROMPT_ATOMICO_INICIAL, input_variables=["solicitacao_usuario", "rag_context"])
155
- yield f"data: {json.dumps({'progress': 15, 'message': 'Iniciando processamento paralelo...'})}\n\n"
 
156
 
157
  for name, llm in models.items():
158
  chain = prompt | llm | output_parser
@@ -166,69 +187,100 @@ def process():
166
  for key, result in results.items():
167
  if result == "Error:EmptyResponse" or "Erro ao processar" in result:
168
  error_msg = result if "Erro ao processar" in result else f"Falha no serviço {key.upper()}: Sem resposta."
169
- yield f"data: {json.dumps({'error': error_msg})}\n\n"
 
170
  return
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
 
189
  else:
190
  # --- LÓGICA HIERÁRQUICA (SEQUENCIAL) ---
191
- yield f"data: {json.dumps({'progress': 15, 'message': 'O GROK está processando sua solicitação...'})}\n\n"
 
 
 
192
  prompt_grok = PromptTemplate(template=PROMPT_HIERARQUICO_GROK, input_variables=["solicitacao_usuario", "rag_context"])
193
  chain_grok = prompt_grok | grok_llm | output_parser
194
  resposta_grok = chain_grok.invoke({"solicitacao_usuario": solicitacao_usuario, "rag_context": rag_context})
195
 
196
  if not resposta_grok or not resposta_grok.strip():
197
- yield f"data: {json.dumps({'error': 'Falha no serviço GROK: Sem resposta.'})}\n\n"
 
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)
206
  chain_sonnet = prompt_sonnet | claude_with_max_tokens | output_parser
207
  resposta_sonnet = chain_sonnet.invoke({"solicitacao_usuario": solicitacao_usuario, "texto_para_analise": resposta_grok})
208
 
209
  if not resposta_sonnet or not resposta_sonnet.strip():
210
- yield f"data: {json.dumps({'error': 'Falha no serviço Claude Sonnet: Sem resposta.'})}\n\n"
 
211
  return
212
 
213
- print(f"--- Resposta Bruta do Sonnet (Hierárquico) ---\n{resposta_sonnet}\n--------------------------------------------")
214
- # CORREÇÃO: Aqui estava o erro - faltava vírgula após 'content': resposta_sonnet
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
219
  resposta_gemini = chain_gemini.invoke({"solicitacao_usuario": solicitacao_usuario, "texto_para_analise": resposta_sonnet})
220
 
221
  if not resposta_gemini or not resposta_gemini.strip():
222
- yield f"data: {json.dumps({'error': 'Falha no serviço Gemini: Sem resposta.'})}\n\n"
 
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}")
231
- yield f"data: {json.dumps({'error': f'Ocorreu um erro inesperado na aplicação: {e}'})}\n\n"
 
232
 
233
  return Response(generate_stream(mode, form_data, temp_file_paths), mimetype='text/event-stream')
234
 
@@ -240,7 +292,8 @@ def merge():
240
  def generate_merge_stream():
241
  """Gera a resposta do merge em streaming."""
242
  try:
243
- yield f"data: {json.dumps({'progress': 0, 'message': 'Iniciando o processo de merge...'})}\n\n"
 
244
 
245
  output_parser = StrOutputParser()
246
  prompt_merge = PromptTemplate(template=PROMPT_ATOMICO_MERGE, input_variables=["solicitacao_usuario", "texto_para_analise_grok", "texto_para_analise_sonnet", "texto_para_analise_gemini"])
@@ -248,7 +301,8 @@ def merge():
248
  grok_with_max_tokens = grok_llm.bind(max_tokens=20000)
249
  chain_merge = prompt_merge | grok_with_max_tokens | output_parser
250
 
251
- yield f"data: {json.dumps({'progress': 50, 'message': 'Enviando textos para o GROK para consolidação...'})}\n\n"
 
252
 
253
  resposta_merge = chain_merge.invoke({
254
  "solicitacao_usuario": data.get('solicitacao_usuario'),
@@ -258,20 +312,22 @@ def merge():
258
  })
259
 
260
  if not resposta_merge or not resposta_merge.strip():
261
- yield f"data: {json.dumps({'error': 'Falha no serviço de Merge (GROK): Sem resposta.'})}\n\n"
 
262
  return
263
 
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}")
272
- yield f"data: {json.dumps({'error': str(e)})}\n\n"
 
273
 
274
  return Response(generate_merge_stream(), mimetype='text/event-stream')
275
 
276
  if __name__ == '__main__':
277
- app.run(debug=True)
 
70
  # 4. Verifica se o texto restante (após remover espaços nas pontas) está de fato vazio
71
  return not normalized_space.strip()
72
 
73
+ def safe_json_dumps(data):
74
+ """Função para criar JSON de forma segura, escapando caracteres problemáticos"""
75
+ try:
76
+ return json.dumps(data, ensure_ascii=False)
77
+ except Exception as e:
78
+ print(f"Erro ao criar JSON: {e}")
79
+ # Fallback: tenta escapar o conteúdo problemático
80
+ if 'content' in str(data):
81
+ # Substitui o conteúdo por uma versão escapada
82
+ safe_data = data.copy() if isinstance(data, dict) else {}
83
+ if 'partial_result' in safe_data and 'content' in safe_data['partial_result']:
84
+ safe_data['partial_result']['content'] = safe_data['partial_result']['content'][:100] + "... [CONTEÚDO TRUNCADO DEVIDO A ERRO DE ENCODING]"
85
+ return json.dumps(safe_data, ensure_ascii=False)
86
+ return json.dumps({'error': 'Erro na serialização JSON'})
87
+
88
  @app.route('/')
89
  def index():
90
  """Renderiza a página inicial da aplicação."""
 
126
  if current_mode == 'test':
127
  mock_text = form_data.get('mock_text', 'Este é um **texto** de `simulação`.')
128
  # MUDANÇA: Envia o texto bruto na simulação
129
+ json_data = safe_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'})
130
+ yield f"data: {json_data}\n\n"
131
  if processing_mode == 'atomic':
132
+ json_data = safe_json_dumps({'partial_result': {'id': 'sonnet-output', 'content': mock_text}})
133
+ yield f"data: {json_data}\n\n"
134
+ json_data = safe_json_dumps({'partial_result': {'id': 'gemini-output', 'content': mock_text}})
135
+ yield f"data: {json_data}\n\n"
136
  else:
137
  if not solicitacao_usuario:
138
+ json_data = safe_json_dumps({'error': 'Solicitação não fornecida.'})
139
+ yield f"data: {json_data}\n\n"
140
  return
141
 
142
  try:
143
+ json_data = safe_json_dumps({'progress': 0, 'message': 'Processando arquivos e extraindo contexto...'})
144
+ yield f"data: {json_data}\n\n"
145
  rag_context = get_relevant_context(file_paths, solicitacao_usuario)
146
 
147
  output_parser = StrOutputParser()
 
172
  models = {'grok': grok_llm, 'sonnet': claude_atomic_llm, 'gemini': gemini_llm}
173
 
174
  prompt = PromptTemplate(template=PROMPT_ATOMICO_INICIAL, input_variables=["solicitacao_usuario", "rag_context"])
175
+ json_data = safe_json_dumps({'progress': 15, 'message': 'Iniciando processamento paralelo...'})
176
+ yield f"data: {json_data}\n\n"
177
 
178
  for name, llm in models.items():
179
  chain = prompt | llm | output_parser
 
187
  for key, result in results.items():
188
  if result == "Error:EmptyResponse" or "Erro ao processar" in result:
189
  error_msg = result if "Erro ao processar" in result else f"Falha no serviço {key.upper()}: Sem resposta."
190
+ json_data = safe_json_dumps({'error': error_msg})
191
+ yield f"data: {json_data}\n\n"
192
  return
193
 
194
+ json_data = safe_json_dumps({'progress': 80, 'message': 'Todos os modelos responderam. Formatando saídas...'})
195
+ yield f"data: {json_data}\n\n"
196
 
197
  # MUDANÇA: Envia o texto bruto para cada modelo
198
  grok_text = results.get('grok', '')
199
  print(f"--- Resposta Bruta do GROK (Atômico) ---\n{grok_text}\n--------------------------------------")
200
+ json_data = safe_json_dumps({'partial_result': {'id': 'grok-output', 'content': grok_text}})
201
+ yield f"data: {json_data}\n\n"
202
 
203
  sonnet_text = results.get('sonnet', '')
204
  print(f"--- Resposta Bruta do Sonnet (Atômico) ---\n{sonnet_text}\n----------------------------------------")
205
+ json_data = safe_json_dumps({'partial_result': {'id': 'sonnet-output', 'content': sonnet_text}})
206
+ yield f"data: {json_data}\n\n"
207
 
208
  gemini_text = results.get('gemini', '')
209
  print(f"--- Resposta Bruta do Gemini (Atômico) ---\n{gemini_text}\n----------------------------------------")
210
+ json_data = safe_json_dumps({'partial_result': {'id': 'gemini-output', 'content': gemini_text}})
211
+ yield f"data: {json_data}\n\n"
212
 
213
+ json_data = safe_json_dumps({'progress': 100, 'message': 'Processamento Atômico concluído!', 'done': True, 'mode': 'atomic'})
214
+ yield f"data: {json_data}\n\n"
215
 
216
  else:
217
  # --- LÓGICA HIERÁRQUICA (SEQUENCIAL) ---
218
+ print("=== INICIANDO MODO HIERÁRQUICO ===")
219
+ json_data = safe_json_dumps({'progress': 15, 'message': 'O GROK está processando sua solicitação...'})
220
+ yield f"data: {json_data}\n\n"
221
+
222
  prompt_grok = PromptTemplate(template=PROMPT_HIERARQUICO_GROK, input_variables=["solicitacao_usuario", "rag_context"])
223
  chain_grok = prompt_grok | grok_llm | output_parser
224
  resposta_grok = chain_grok.invoke({"solicitacao_usuario": solicitacao_usuario, "rag_context": rag_context})
225
 
226
  if not resposta_grok or not resposta_grok.strip():
227
+ json_data = safe_json_dumps({'error': 'Falha no serviço GROK: Sem resposta.'})
228
+ yield f"data: {json_data}\n\n"
229
  return
230
 
231
+ print(f"--- Resposta Bruta do GROK (Hierárquico) ---\nTamanho: {len(resposta_grok)} caracteres\nPrimeiros 200 chars: {resposta_grok[:200]}...\n------------------------------------------")
 
 
232
 
233
+ # Enviando resposta do GROK
234
+ json_data = safe_json_dumps({'progress': 33, 'message': 'Claude Sonnet está processando...', 'partial_result': {'id': 'grok-output', 'content': resposta_grok}})
235
+ print(f"=== JSON DO GROK CRIADO COM SUCESSO ===")
236
+ yield f"data: {json_data}\n\n"
237
+
238
+ print("=== INICIANDO PROCESSAMENTO DO SONNET ===")
239
  prompt_sonnet = PromptTemplate(template=PROMPT_HIERARQUICO_SONNET, input_variables=["solicitacao_usuario", "texto_para_analise"])
240
  claude_with_max_tokens = claude_llm.bind(max_tokens=20000)
241
  chain_sonnet = prompt_sonnet | claude_with_max_tokens | output_parser
242
  resposta_sonnet = chain_sonnet.invoke({"solicitacao_usuario": solicitacao_usuario, "texto_para_analise": resposta_grok})
243
 
244
  if not resposta_sonnet or not resposta_sonnet.strip():
245
+ json_data = safe_json_dumps({'error': 'Falha no serviço Claude Sonnet: Sem resposta.'})
246
+ yield f"data: {json_data}\n\n"
247
  return
248
 
249
+ print(f"--- Resposta Bruta do Sonnet (Hierárquico) ---\nTamanho: {len(resposta_sonnet)} caracteres\nPrimeiros 200 chars: {resposta_sonnet[:200]}...\n--------------------------------------------")
250
+
251
+ # Tentando criar JSON do SONNET
252
+ print("=== TENTANDO CRIAR JSON DO SONNET ===")
253
+ try:
254
+ sonnet_json_data = {'progress': 66, 'message': 'Gemini está processando...', 'partial_result': {'id': 'sonnet-output', 'content': resposta_sonnet}}
255
+ json_data = safe_json_dumps(sonnet_json_data)
256
+ print(f"=== JSON DO SONNET CRIADO COM SUCESSO ===")
257
+ yield f"data: {json_data}\n\n"
258
+ except Exception as json_error:
259
+ print(f"=== ERRO AO CRIAR JSON DO SONNET: {json_error} ===")
260
+ # Fallback: enviar com conteúdo truncado
261
+ fallback_data = {'progress': 66, 'message': 'Gemini está processando...', 'partial_result': {'id': 'sonnet-output', 'content': resposta_sonnet[:1000] + "... [TRUNCADO]"}}
262
+ json_data = safe_json_dumps(fallback_data)
263
+ yield f"data: {json_data}\n\n"
264
 
265
+ print("=== INICIANDO PROCESSAMENTO DO GEMINI ===")
266
  prompt_gemini = PromptTemplate(template=PROMPT_HIERARQUICO_GEMINI, input_variables=["solicitacao_usuario", "texto_para_analise"])
267
  chain_gemini = prompt_gemini | gemini_llm | output_parser
268
  resposta_gemini = chain_gemini.invoke({"solicitacao_usuario": solicitacao_usuario, "texto_para_analise": resposta_sonnet})
269
 
270
  if not resposta_gemini or not resposta_gemini.strip():
271
+ json_data = safe_json_dumps({'error': 'Falha no serviço Gemini: Sem resposta.'})
272
+ yield f"data: {json_data}\n\n"
273
  return
274
 
275
+ print(f"--- Resposta Bruta do Gemini (Hierárquico) ---\nTamanho: {len(resposta_gemini)} caracteres\nPrimeiros 200 chars: {resposta_gemini[:200]}...\n--------------------------------------------")
276
+
277
+ json_data = safe_json_dumps({'progress': 100, 'message': 'Processamento concluído!', 'partial_result': {'id': 'gemini-output', 'content': resposta_gemini}, 'done': True, 'mode': 'hierarchical'})
278
+ yield f"data: {json_data}\n\n"
279
 
280
  except Exception as e:
281
  print(f"Ocorreu um erro durante o processamento: {e}")
282
+ json_data = safe_json_dumps({'error': f'Ocorreu um erro inesperado na aplicação: {e}'})
283
+ yield f"data: {json_data}\n\n"
284
 
285
  return Response(generate_stream(mode, form_data, temp_file_paths), mimetype='text/event-stream')
286
 
 
292
  def generate_merge_stream():
293
  """Gera a resposta do merge em streaming."""
294
  try:
295
+ json_data = safe_json_dumps({'progress': 0, 'message': 'Iniciando o processo de merge...'})
296
+ yield f"data: {json_data}\n\n"
297
 
298
  output_parser = StrOutputParser()
299
  prompt_merge = PromptTemplate(template=PROMPT_ATOMICO_MERGE, input_variables=["solicitacao_usuario", "texto_para_analise_grok", "texto_para_analise_sonnet", "texto_para_analise_gemini"])
 
301
  grok_with_max_tokens = grok_llm.bind(max_tokens=20000)
302
  chain_merge = prompt_merge | grok_with_max_tokens | output_parser
303
 
304
+ json_data = safe_json_dumps({'progress': 50, 'message': 'Enviando textos para o GROK para consolidação...'})
305
+ yield f"data: {json_data}\n\n"
306
 
307
  resposta_merge = chain_merge.invoke({
308
  "solicitacao_usuario": data.get('solicitacao_usuario'),
 
312
  })
313
 
314
  if not resposta_merge or not resposta_merge.strip():
315
+ json_data = safe_json_dumps({'error': 'Falha no serviço de Merge (GROK): Sem resposta.'})
316
+ yield f"data: {json_data}\n\n"
317
  return
318
 
319
  print(f"--- Resposta Bruta do Merge (GROK) ---\n{resposta_merge}\n------------------------------------")
320
  word_count = len(resposta_merge.split())
321
 
322
+ json_data = safe_json_dumps({'progress': 100, 'message': 'Merge concluído!', 'final_result': {'content': resposta_merge, 'word_count': word_count}, 'done': True})
323
+ yield f"data: {json_data}\n\n"
324
 
325
  except Exception as e:
326
  print(f"Erro no processo de merge: {e}")
327
+ json_data = safe_json_dumps({'error': str(e)})
328
+ yield f"data: {json_data}\n\n"
329
 
330
  return Response(generate_merge_stream(), mimetype='text/event-stream')
331
 
332
  if __name__ == '__main__':
333
+ app.run(debug=True)
templates/index.html CHANGED
@@ -6,11 +6,11 @@
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;
@@ -20,6 +20,10 @@
20
  .convert-btn:hover {
21
  background-color: #138496;
22
  }
 
 
 
 
23
  </style>
24
  </head>
25
  <body>
@@ -28,19 +32,24 @@
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,16 +69,17 @@
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>
@@ -88,7 +98,7 @@ Use o botão "Converter para MD" para ver o resultado formatado.</textarea>
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>
@@ -106,9 +116,22 @@ Use o botão "Converter para MD" para ver o resultado formatado.</textarea>
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,47 +142,41 @@ Use o botão "Converter para MD" para ver o resultado formatado.</textarea>
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,84 +186,127 @@ Use o botão "Converter para MD" para ver o resultado formatado.</textarea>
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,45 +316,72 @@ Use o botão "Converter para MD" para ver o resultado formatado.</textarea>
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
  }
275
 
 
276
  function showError(message) {
277
  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>`;
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', {
@@ -308,12 +395,12 @@ Use o botão "Converter para MD" para ver o resultado formatado.</textarea>
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>
 
6
  <title>Sistema Multi-Agente de IA</title>
7
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
8
  <style>
9
+ /* Estilos para o novo botão de conversão */
10
  .convert-btn {
11
  padding: 5px 10px;
12
  font-size: 12px;
13
+ background-color: #17a2b8; /* Cor Ciano/Azul claro */
14
  color: white;
15
  border: none;
16
  border-radius: 4px;
 
20
  .convert-btn:hover {
21
  background-color: #138496;
22
  }
23
+ .convert-btn:disabled {
24
+ background-color: #5a6268;
25
+ cursor: not-allowed;
26
+ }
27
  </style>
28
  </head>
29
  <body>
 
32
  <div class="loader-content">
33
  <div class="loader-spinner"></div>
34
  <p id="loader-message">Processando sua solicitação...</p>
35
+ <div class="progress-bar-container"><div id="progress-bar" class="progress-bar"></div></div>
 
 
36
  </div>
37
  </div>
38
 
39
+ <button id="merge-btn" class="floating-merge-btn" style="display: none;">Processar Merge</button>
40
+
41
  <div class="container">
42
  <div class="header-container">
43
  <div>
44
  <h1>Sistema Multi-Agente IA</h1>
45
+ <p id="flow-description">GROK ➔ Claude Sonnet ➔ Gemini</p>
46
  </div>
47
  <div class="controls-container">
48
+ <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.">
49
+ <span>Hierárquico</span>
50
+ <label class="switch"><input type="checkbox" id="processing-mode-switch"><span class="slider round"></span></label>
51
+ <span>Atômico</span>
52
+ </div>
53
  <div class="mode-toggle">
54
  <span>Modo Real</span>
55
  <label class="switch"><input type="checkbox" id="mode-switch"><span class="slider round"></span></label>
 
69
  <button type="submit">Processar com IA</button>
70
  </form>
71
  </div>
 
72
  <div id="mock-form-container" style="display: none;">
73
  <form id="request-form-mock">
74
  <label for="mock_text">Cole o texto de simulação aqui:</label>
75
+ <textarea name="mock_text" id="mock_text" rows="10" required>### Título
76
+
77
+ Este é um exemplo de texto **bruto** em Markdown.
78
 
79
  - Item 1
80
  - Item 2
81
 
82
+ Use o botão `Converter para MD` para ver a mágica.</textarea>
83
  <button type="submit">Simular Resposta</button>
84
  </form>
85
  </div>
 
98
  <div class="result-column">
99
  <div class="column-header">
100
  <h2>Claude Sonnet</h2>
101
+ <div>
102
  <button class="copy-btn" onclick="copyToClipboard('sonnet-output')">Copiar</button>
103
  <button class="convert-btn" onclick="convertToMarkdown('sonnet-output')">Converter para MD</button>
104
  </div>
 
116
  <div class="output-box" id="gemini-output"></div>
117
  </div>
118
  </div>
119
+
120
+ <div id="final-result-container" style="display: none;">
121
+ <div class="column-header" style="border-radius: 8px 8px 0 0; background-color: #e9ecef;">
122
+ <h2 id="final-result-title">Texto Final</h2>
123
+ <div>
124
+ <button class="copy-btn" onclick="copyToClipboard('final-output')">Copiar</button>
125
+ <button class="convert-btn" onclick="convertToMarkdown('final-output')">Converter para MD</button>
126
+ </div>
127
+ </div>
128
+ <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>
129
+ </div>
130
  </div>
131
 
132
  <script>
133
+ // --- Variáveis Globais ---
134
+ const processingModeSwitch = document.getElementById('processing-mode-switch');
135
  const modeSwitch = document.getElementById('mode-switch');
136
  const realContainer = document.getElementById('real-form-container');
137
  const mockContainer = document.getElementById('mock-form-container');
 
142
  const errorContainer = document.getElementById('error-box-container');
143
  const textarea = document.getElementById('solicitacao_usuario');
144
  const fileList = document.getElementById('file-list');
145
+ const mergeBtn = document.getElementById('merge-btn');
146
+ const finalResultContainer = document.getElementById('final-result-container');
147
+ const finalOutput = document.getElementById('final-output');
148
  let attachedFiles = [];
149
+ let originalUserQuery = "";
150
+ let rawTexts = {}; // NOVO: Objeto para guardar os textos brutos
151
 
152
+ // --- Lógica de UI ---
 
 
153
  modeSwitch.addEventListener('change', function() {
154
  realContainer.style.display = this.checked ? 'none' : 'block';
155
  mockContainer.style.display = this.checked ? 'block' : 'none';
156
  });
157
+ processingModeSwitch.addEventListener('change', function() {
158
+ document.getElementById('flow-description').textContent = this.checked ? "GROK | Claude Sonnet | Gemini (Paralelo)" : "GROK ➔ Claude Sonnet ➔ Gemini";
159
+ });
160
+
161
+ // --- Lógica de Upload ---
162
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
163
  textarea.addEventListener(eventName, preventDefaults, false);
164
  document.body.addEventListener(eventName, preventDefaults, false);
165
  });
166
+ ['dragenter', 'dragover'].forEach(eventName => textarea.addEventListener(eventName, () => textarea.classList.add('drag-over'), false));
167
+ ['dragleave', 'drop'].forEach(eventName => textarea.addEventListener(eventName, () => textarea.classList.remove('drag-over'), false));
 
 
 
 
168
  textarea.addEventListener('drop', handleDrop, false);
 
169
  function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
170
  function handleDrop(e) { handleFiles(e.dataTransfer.files); }
 
171
  function handleFiles(files) {
172
  [...files].forEach(file => {
173
  const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'];
174
+ if (!allowedTypes.includes(file.type)) { showError(`Formato não suportado: ${file.name}`); return; }
175
+ if (file.size > 100 * 1024 * 1024) { showError(`Arquivo muito grande: ${file.name}`); return; }
 
 
 
 
 
 
176
  attachedFiles.push(file);
177
  });
178
  updateFileList();
179
  }
 
180
  function updateFileList() {
181
  fileList.innerHTML = '';
182
  document.getElementById('file-list-container').style.display = attachedFiles.length > 0 ? 'block' : 'none';
 
186
  const removeBtn = document.createElement('span');
187
  removeBtn.textContent = '×';
188
  removeBtn.className = 'remove-file-btn';
189
+ removeBtn.onclick = () => { attachedFiles.splice(index, 1); updateFileList(); };
 
 
 
190
  li.appendChild(removeBtn);
191
  fileList.appendChild(li);
192
  });
193
  }
194
 
195
+ // --- Lógica de Submissão Principal ---
196
  document.getElementById('request-form-real').addEventListener('submit', handleFormSubmit);
197
  document.getElementById('request-form-mock').addEventListener('submit', handleFormSubmit);
198
 
199
  async function handleFormSubmit(event) {
200
  event.preventDefault();
201
+ // Resetar a interface
202
  errorContainer.innerHTML = '';
203
  resultsContainer.style.display = 'none';
204
+ finalResultContainer.style.display = 'none';
205
+ mergeBtn.style.display = 'none';
206
  document.querySelectorAll('.output-box').forEach(box => box.innerHTML = '');
207
+ document.querySelectorAll('.convert-btn').forEach(btn => {
208
+ btn.disabled = false;
209
+ btn.innerText = 'Converter para MD';
210
+ });
211
+ rawTexts = {}; // Limpa os textos brutos
212
+
213
+ // Iniciar o loader
214
  loaderMessage.textContent = 'Iniciando conexão...';
215
  progressBar.style.width = '0%';
216
  loader.style.display = 'flex';
217
+
218
  const formData = new FormData();
219
+ formData.append('processing_mode', processingModeSwitch.checked ? 'atomic' : 'hierarchical');
220
  if (modeSwitch.checked) {
221
  formData.append('mode', 'test');
222
  formData.append('mock_text', document.getElementById('mock_text').value);
223
+ originalUserQuery = "Simulação de teste.";
224
  } else {
225
  formData.append('mode', 'real');
226
+ originalUserQuery = document.getElementById('solicitacao_usuario').value;
227
+ formData.append('solicitacao', originalUserQuery);
228
+ attachedFiles.forEach(file => { formData.append('files', file); });
 
229
  }
230
+ try {
231
+ const response = await fetch('/process', { method: 'POST', body: formData });
232
+ if (!response.ok || !response.body) throw new Error(`Erro na resposta do servidor: ${response.statusText}`);
233
+
234
+ const reader = response.body.getReader();
235
+ const decoder = new TextDecoder();
236
+ while (true) {
237
+ const { done, value } = await reader.read();
238
+ if (done) break;
239
+ const chunk = decoder.decode(value, { stream: true });
240
+ const lines = chunk.split('\n\n');
241
+ lines.forEach(line => {
242
+ if (line.startsWith('data: ')) {
243
+ const jsonData = line.substring(6);
244
+ if (jsonData.trim()) {
245
+ try {
246
+ const data = JSON.parse(jsonData);
247
+ processStreamData(data, false);
248
+ } catch (e) { console.error("Erro ao parsear JSON:", jsonData); }
249
+ }
250
+ }
251
+ });
252
+ }
253
+ } catch (error) {
254
+ showError('A conexão com o servidor falhou.');
255
+ loader.style.display = 'none';
256
+ console.error("Fetch Error:", error);
257
+ }
258
+ }
259
+
260
+ // --- Lógica do Botão de Merge ---
261
+ mergeBtn.addEventListener('click', async function() {
262
+ loaderMessage.textContent = 'Processando o merge dos textos...';
263
+ progressBar.style.width = '0%';
264
+ loader.style.display = 'flex';
265
+ this.style.display = 'none';
266
+
267
+ // Sempre pega o texto bruto para o merge
268
+ const payload = {
269
+ solicitacao_usuario: originalUserQuery,
270
+ grok_text: rawTexts['grok-output'] || '',
271
+ sonnet_text: rawTexts['sonnet-output'] || '',
272
+ gemini_text: rawTexts['gemini-output'] || '',
273
+ };
274
 
275
  try {
276
+ const response = await fetch('/merge', {
277
  method: 'POST',
278
+ headers: { 'Content-Type': 'application/json' },
279
+ body: JSON.stringify(payload)
280
  });
281
+ if (!response.ok || !response.body) throw new Error(`Erro na resposta do servidor: ${response.statusText}`);
282
+
 
 
 
283
  const reader = response.body.getReader();
284
  const decoder = new TextDecoder();
 
285
  while (true) {
286
  const { done, value } = await reader.read();
287
  if (done) break;
 
288
  const chunk = decoder.decode(value, { stream: true });
289
  const lines = chunk.split('\n\n');
 
290
  lines.forEach(line => {
291
  if (line.startsWith('data: ')) {
292
  const jsonData = line.substring(6);
293
  if (jsonData.trim()) {
294
  try {
295
  const data = JSON.parse(jsonData);
296
+ processStreamData(data, true); // true = é merge
297
+ } catch (e) { console.error("Erro ao parsear JSON do merge:", jsonData); }
 
 
298
  }
299
  }
300
  });
301
  }
302
  } catch (error) {
303
+ showError("A conexão falhou ao tentar processar o merge.");
304
  loader.style.display = 'none';
 
305
  }
306
+ });
307
 
308
+ // --- Função de Processamento de Stream (ATUALIZADA) ---
309
+ function processStreamData(data, isMerge) {
310
  if (data.error) {
311
  showError(data.error);
312
  loader.style.display = 'none';
 
316
  loaderMessage.textContent = data.message;
317
  progressBar.style.width = data.progress + '%';
318
 
319
+ const processContent = (targetId, content, wordCount = null) => {
320
+ const targetBox = document.getElementById(targetId);
321
+ if (!targetBox) return;
322
+
323
+ // Armazena o texto bruto e exibe na tela
324
+ rawTexts[targetId] = content;
325
+ targetBox.innerText = content;
326
+
327
+ if (targetId === 'final-output') {
328
+ const finalTitle = document.getElementById('final-result-title');
329
+ finalTitle.textContent = `Texto Final`;
330
+ if (wordCount) {
331
+ finalTitle.textContent += ` (${wordCount} palavras)`;
332
+ }
333
+ finalResultContainer.style.display = 'block';
334
+ } else {
335
+ resultsContainer.style.display = 'flex';
336
  }
337
+ };
338
+
339
+ if (isMerge && data.final_result) {
340
+ processContent('final-output', data.final_result.content, data.final_result.word_count);
341
+ } else if (data.partial_result) {
342
+ processContent(data.partial_result.id, data.partial_result.content);
343
  }
344
 
345
  if (data.done) {
346
  setTimeout(() => {
347
  loader.style.display = 'none';
348
+ if (data.mode === 'atomic' && !isMerge) {
349
+ mergeBtn.style.display = 'block';
350
+ }
351
+ }, 500);
352
  }
353
  }
354
 
355
+ // --- Funções de Utilitários (ATUALIZADAS) ---
356
  function showError(message) {
357
  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>`;
358
  }
359
 
360
  function copyToClipboard(elementId) {
361
+ // Sempre copia o texto bruto original do objeto rawTexts
362
  const textToCopy = rawTexts[elementId];
363
+ if (textToCopy !== undefined) {
364
+ navigator.clipboard.writeText(textToCopy).then(() => {
365
+ alert('Texto copiado!');
366
  });
367
+ } else {
368
+ alert('Nenhum texto para copiar.');
369
  }
370
  }
371
 
372
+ // NOVA FUNÇÃO para converter para Markdown sob demanda
373
  async function convertToMarkdown(elementId) {
374
  const button = event.target;
375
+ button.disabled = true;
376
  button.innerText = 'Convertendo...';
377
 
378
  const rawText = rawTexts[elementId];
379
+ if (rawText === undefined) {
380
+ showError('Não há texto para converter.');
381
+ button.innerText = 'Converter para MD';
382
+ button.disabled = false;
383
+ return;
384
+ }
385
 
386
  try {
387
  const response = await fetch('/convert', {
 
395
  const data = await response.json();
396
  const targetBox = document.getElementById(elementId);
397
  targetBox.innerHTML = data.html; // Insere o HTML convertido
398
+ button.innerText = 'Convertido'; // Mantém o botão desabilitado para indicar sucesso
399
  } catch (error) {
400
  showError('Não foi possível converter o texto.');
401
  console.error('Conversion error:', error);
402
+ button.innerText = 'Converter para MD'; // Reabilita em caso de erro
403
+ button.disabled = false;
 
404
  }
405
  }
406
  </script>