victorafarias commited on
Commit
0e0a17d
·
1 Parent(s): 3c12f7f

Evolução: modo Hierarquico e Atomico

Browse files
Files changed (4) hide show
  1. app.py +103 -53
  2. config.py +95 -12
  3. static/style.css +47 -0
  4. templates/index.html +95 -61
app.py CHANGED
@@ -1,11 +1,12 @@
1
  # app.py
2
 
3
- from flask import Flask, render_template, request, Response
4
  import markdown2
5
  import json
6
  import time
7
  import os
8
  import uuid
 
9
 
10
  # Importações do LangChain
11
  from langchain.prompts import PromptTemplate
@@ -15,7 +16,7 @@ from langchain.chains import LLMChain
15
  from llms import claude_llm, grok_llm, gemini_llm
16
 
17
  # Importa os prompts
18
- from config import PROMPT_CLAUDE_SONNET, PROMPT_GROK, PROMPT_GEMINI
19
 
20
  # Importa nosso processador RAG
21
  from rag_processor import get_relevant_context
@@ -24,8 +25,6 @@ app = Flask(__name__)
24
 
25
  app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
26
 
27
- # A linha 'os.makedirs('uploads')' foi removida daqui, pois agora é gerenciada pelo Dockerfile.
28
-
29
  @app.route('/')
30
  def index():
31
  return render_template('index.html')
@@ -35,6 +34,7 @@ def process():
35
  form_data = request.form
36
  files = request.files.getlist('files')
37
  mode = form_data.get('mode', 'real')
 
38
 
39
  temp_file_paths = []
40
  if mode == 'real':
@@ -46,20 +46,15 @@ def process():
46
  temp_file_paths.append(file_path)
47
 
48
  def generate_stream(current_mode, form_data, file_paths):
 
 
49
  if current_mode == 'test':
 
50
  mock_text = form_data.get('mock_text', 'Este é um texto de simulação.')
51
  mock_html = markdown2.markdown(mock_text, extras=["fenced-code-blocks", "tables"])
52
-
53
- yield f"data: {json.dumps({'progress': 0, 'message': 'Simulando Etapa 1: GROK...'})}\n\n"
54
- time.sleep(1)
55
- yield f"data: {json.dumps({'progress': 33, 'message': 'Simulando Etapa 2: Claude Sonnet...', 'partial_result': {'id': 'grok-output', 'content': mock_html}})}\n\n"
56
- time.sleep(1)
57
- yield f"data: {json.dumps({'progress': 66, 'message': 'Simulando Etapa 3: Gemini...', 'partial_result': {'id': 'sonnet-output', 'content': mock_html}})}\n\n"
58
- time.sleep(1)
59
- yield f"data: {json.dumps({'progress': 100, 'message': 'Simulação concluída!', 'partial_result': {'id': 'gemini-output', 'content': mock_html}, 'done': True})}\n\n"
60
 
61
  else:
62
- solicitacao_usuario = form_data.get('solicitacao', '')
63
  if not solicitacao_usuario:
64
  yield f"data: {json.dumps({'error': 'Solicitação não fornecida.'})}\n\n"
65
  return
@@ -68,46 +63,70 @@ def process():
68
  yield f"data: {json.dumps({'progress': 0, 'message': 'Processando arquivos e extraindo contexto...'})}\n\n"
69
  rag_context = get_relevant_context(file_paths, solicitacao_usuario)
70
 
71
- # --- ETAPA 1: GROK ---
72
- yield f"data: {json.dumps({'progress': 15, 'message': 'O GROK está processando sua solicitação com os arquivos...'})}\n\n"
73
- prompt_grok = PromptTemplate(template=PROMPT_GROK, input_variables=["solicitacao_usuario", "rag_context"])
74
- chain_grok = LLMChain(llm=grok_llm, prompt=prompt_grok)
75
- resposta_grok = chain_grok.invoke({"solicitacao_usuario": solicitacao_usuario, "rag_context": rag_context})['text']
76
-
77
- # NOVA VALIDAÇÃO: Verifica se a resposta do GROK está vazia
78
- if not resposta_grok or not resposta_grok.strip():
79
- yield f"data: {json.dumps({'error': 'Falha no serviço GROK: Sem resposta.'})}\n\n"
80
- return
81
-
82
- grok_html = markdown2.markdown(resposta_grok, extras=["fenced-code-blocks", "tables"])
83
- yield f"data: {json.dumps({'progress': 33, 'message': 'Agora, o Claude Sonnet está aprofundando o texto...', 'partial_result': {'id': 'grok-output', 'content': grok_html}})}\n\n"
84
-
85
- # --- ETAPA 2: Claude Sonnet ---
86
- prompt_sonnet = PromptTemplate(template=PROMPT_CLAUDE_SONNET, input_variables=["solicitacao_usuario", "texto_para_analise"])
87
- claude_with_max_tokens = claude_llm.bind(max_tokens=8000)
88
- chain_sonnet = LLMChain(llm=claude_with_max_tokens, prompt=prompt_sonnet)
89
- resposta_sonnet = chain_sonnet.invoke({"solicitacao_usuario": solicitacao_usuario, "texto_para_analise": resposta_grok})['text']
90
-
91
- # NOVA VALIDAÇÃO: Verifica se a resposta do Claude Sonnet está vazia
92
- if not resposta_sonnet or not resposta_sonnet.strip():
93
- yield f"data: {json.dumps({'error': 'Falha no serviço Claude Sonnet: Sem resposta.'})}\n\n"
94
- return
95
-
96
- sonnet_html = markdown2.markdown(resposta_sonnet, extras=["fenced-code-blocks", "tables"])
97
- yield f"data: {json.dumps({'progress': 66, 'message': 'Estamos quase lá! Seu texto está passando por uma revisão final com o Gemini...', 'partial_result': {'id': 'sonnet-output', 'content': sonnet_html}})}\n\n"
98
-
99
- # --- ETAPA 3: Gemini ---
100
- prompt_gemini = PromptTemplate(template=PROMPT_GEMINI, input_variables=["solicitacao_usuario", "texto_para_analise"])
101
- chain_gemini = LLMChain(llm=gemini_llm, prompt=prompt_gemini)
102
- resposta_gemini = chain_gemini.invoke({"solicitacao_usuario": solicitacao_usuario, "texto_para_analise": resposta_sonnet})['text']
103
-
104
- # NOVA VALIDAÇÃO: Verifica se a resposta do Gemini está vazia
105
- if not resposta_gemini or not resposta_gemini.strip():
106
- yield f"data: {json.dumps({'error': 'Falha no serviço Gemini: Sem resposta.'})}\n\n"
107
- return
108
-
109
- gemini_html = markdown2.markdown(resposta_gemini, extras=["fenced-code-blocks", "tables"])
110
- yield f"data: {json.dumps({'progress': 100, 'message': 'Processamento concluído!', 'partial_result': {'id': 'gemini-output', 'content': gemini_html}, 'done': True})}\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  except Exception as e:
113
  print(f"Ocorreu um erro durante o processamento: {e}")
@@ -115,5 +134,36 @@ def process():
115
 
116
  return Response(generate_stream(mode, form_data, temp_file_paths), mimetype='text/event-stream')
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  if __name__ == '__main__':
119
  app.run(debug=True)
 
1
  # app.py
2
 
3
+ from flask import Flask, render_template, request, Response, jsonify
4
  import markdown2
5
  import json
6
  import time
7
  import os
8
  import uuid
9
+ import threading # Necessário para o processamento em paralelo
10
 
11
  # Importações do LangChain
12
  from langchain.prompts import PromptTemplate
 
16
  from llms import claude_llm, grok_llm, gemini_llm
17
 
18
  # Importa os prompts
19
+ from config import *
20
 
21
  # Importa nosso processador RAG
22
  from rag_processor import get_relevant_context
 
25
 
26
  app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
27
 
 
 
28
  @app.route('/')
29
  def index():
30
  return render_template('index.html')
 
34
  form_data = request.form
35
  files = request.files.getlist('files')
36
  mode = form_data.get('mode', 'real')
37
+ processing_mode = form_data.get('processing_mode', 'hierarchical')
38
 
39
  temp_file_paths = []
40
  if mode == 'real':
 
46
  temp_file_paths.append(file_path)
47
 
48
  def generate_stream(current_mode, form_data, file_paths):
49
+ solicitacao_usuario = form_data.get('solicitacao', '')
50
+
51
  if current_mode == 'test':
52
+ # Lógica de simulação (simplificada para focar na lógica real)
53
  mock_text = form_data.get('mock_text', 'Este é um texto de simulação.')
54
  mock_html = markdown2.markdown(mock_text, extras=["fenced-code-blocks", "tables"])
55
+ 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"
 
 
 
 
 
 
 
56
 
57
  else:
 
58
  if not solicitacao_usuario:
59
  yield f"data: {json.dumps({'error': 'Solicitação não fornecida.'})}\n\n"
60
  return
 
63
  yield f"data: {json.dumps({'progress': 0, 'message': 'Processando arquivos e extraindo contexto...'})}\n\n"
64
  rag_context = get_relevant_context(file_paths, solicitacao_usuario)
65
 
66
+ if processing_mode == 'atomic':
67
+ # --- LÓGICA ATÔMICA (PARALELA) ---
68
+ results = {}
69
+ threads = []
70
+
71
+ def run_chain(chain, inputs, key):
72
+ try:
73
+ results[key] = chain.invoke(inputs)['text']
74
+ except Exception as e:
75
+ results[key] = f"Erro ao processar {key}: {e}"
76
+
77
+ # Configurar e iniciar threads
78
+ models = {'grok': grok_llm, 'sonnet': claude_llm, 'gemini': gemini_llm}
79
+ prompt = PromptTemplate(template=PROMPT_ATOMICO_INICIAL, input_variables=["solicitacao_usuario", "rag_context"])
80
+
81
+ for name, llm in models.items():
82
+ chain = LLMChain(llm=llm, prompt=prompt)
83
+ thread = threading.Thread(target=run_chain, args=(chain, {"solicitacao_usuario": solicitacao_usuario, "rag_context": rag_context}, name))
84
+ threads.append(thread)
85
+ thread.start()
86
+
87
+ # Monitorar e enviar resultados conforme chegam
88
+ completed_threads = 0
89
+ while completed_threads < len(threads):
90
+ for i, thread in enumerate(threads):
91
+ key = list(models.keys())[i]
92
+ if not thread.is_alive() and key not in results:
93
+ # Se a thread terminou mas não há resultado, algo deu errado
94
+ results[key] = f"Falha na thread para {key}."
95
+
96
+ if key in results:
97
+ html_content = markdown2.markdown(results[key], extras=["fenced-code-blocks", "tables"])
98
+ yield f"data: {json.dumps({'progress': int((len(results)/len(threads))*100), 'message': f'Modelo {key.upper()} concluiu.', 'partial_result': {'id': f'{key}-output', 'content': html_content}})}\n\n"
99
+ threads.pop(i) # Remove para não processar de novo
100
+ completed_threads += 1
101
+ time.sleep(1)
102
+
103
+ yield f"data: {json.dumps({'progress': 100, 'message': 'Processamento Atômico concluído!', 'done': True, 'mode': 'atomic'})}\n\n"
104
+
105
+ else:
106
+ # --- LÓGICA HIERÁRQUICA (SEQUENCIAL) ---
107
+ # (Mesma lógica de antes, com nomes de prompts atualizados)
108
+ yield f"data: {json.dumps({'progress': 15, 'message': 'O GROK está processando sua solicitação com os arquivos...'})}\n\n"
109
+ prompt_grok = PromptTemplate(template=PROMPT_HIERARQUICO_GROK, input_variables=["solicitacao_usuario", "rag_context"])
110
+ chain_grok = LLMChain(llm=grok_llm, prompt=prompt_grok)
111
+ resposta_grok = chain_grok.invoke({"solicitacao_usuario": solicitacao_usuario, "rag_context": rag_context})['text']
112
+ if not resposta_grok or not resposta_grok.strip(): raise ValueError("Falha no serviço GROK: Sem resposta.")
113
+ grok_html = markdown2.markdown(resposta_grok, extras=["fenced-code-blocks", "tables"])
114
+ yield f"data: {json.dumps({'progress': 33, 'message': 'Agora, o Claude Sonnet está aprofundando o texto...', 'partial_result': {'id': 'grok-output', 'content': grok_html}})}\n\n"
115
+
116
+ prompt_sonnet = PromptTemplate(template=PROMPT_HIERARQUICO_SONNET, input_variables=["solicitacao_usuario", "texto_para_analise"])
117
+ claude_with_max_tokens = claude_llm.bind(max_tokens=8000)
118
+ chain_sonnet = LLMChain(llm=claude_with_max_tokens, prompt=prompt_sonnet)
119
+ resposta_sonnet = chain_sonnet.invoke({"solicitacao_usuario": solicitacao_usuario, "texto_para_analise": resposta_grok})['text']
120
+ if not resposta_sonnet or not resposta_sonnet.strip(): raise ValueError("Falha no serviço Claude Sonnet: Sem resposta.")
121
+ sonnet_html = markdown2.markdown(resposta_sonnet, extras=["fenced-code-blocks", "tables"])
122
+ yield f"data: {json.dumps({'progress': 66, 'message': 'Estamos quase lá! Seu texto está passando por uma revisão final com o Gemini...', 'partial_result': {'id': 'sonnet-output', 'content': sonnet_html}})}\n\n"
123
+
124
+ prompt_gemini = PromptTemplate(template=PROMPT_HIERARQUICO_GEMINI, input_variables=["solicitacao_usuario", "texto_para_analise"])
125
+ chain_gemini = LLMChain(llm=gemini_llm, prompt=prompt_gemini)
126
+ resposta_gemini = chain_gemini.invoke({"solicitacao_usuario": solicitacao_usuario, "texto_para_analise": resposta_sonnet})['text']
127
+ if not resposta_gemini or not resposta_gemini.strip(): raise ValueError("Falha no serviço Gemini: Sem resposta.")
128
+ gemini_html = markdown2.markdown(resposta_gemini, extras=["fenced-code-blocks", "tables"])
129
+ 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"
130
 
131
  except Exception as e:
132
  print(f"Ocorreu um erro durante o processamento: {e}")
 
134
 
135
  return Response(generate_stream(mode, form_data, temp_file_paths), mimetype='text/event-stream')
136
 
137
+ # --- NOVA ROTA PARA O MERGE ---
138
+ @app.route('/merge', methods=['POST'])
139
+ def merge():
140
+ data = request.get_json()
141
+ try:
142
+ # Cria o prompt e a chain para o merge
143
+ prompt_merge = PromptTemplate(template=PROMPT_ATOMICO_MERGE, input_variables=["solicitacao_usuario", "texto_para_analise_grok", "texto_para_analise_sonnet", "texto_para_analise_gemini"])
144
+
145
+ # O merge será feito pelo Claude Sonnet com limite de tokens alto
146
+ claude_with_max_tokens = claude_llm.bind(max_tokens=8000)
147
+ chain_merge = LLMChain(llm=claude_with_max_tokens, prompt=prompt_merge)
148
+
149
+ # Invoca a chain com os dados recebidos
150
+ resposta_merge = chain_merge.invoke({
151
+ "solicitacao_usuario": data.get('solicitacao_usuario'),
152
+ "texto_para_analise_grok": data.get('grok_text'),
153
+ "texto_para_analise_sonnet": data.get('sonnet_text'),
154
+ "texto_para_analise_gemini": data.get('gemini_text')
155
+ })['text']
156
+
157
+ if not resposta_merge or not resposta_merge.strip():
158
+ raise ValueError("Falha no serviço de Merge (Claude Sonnet): Sem resposta.")
159
+
160
+ # Retorna o resultado como HTML
161
+ merge_html = markdown2.markdown(resposta_merge, extras=["fenced-code-blocks", "tables"])
162
+ return jsonify({"success": True, "content": merge_html})
163
+
164
+ except Exception as e:
165
+ print(f"Erro no processo de merge: {e}")
166
+ return jsonify({"success": False, "error": str(e)}), 500
167
+
168
  if __name__ == '__main__':
169
  app.run(debug=True)
config.py CHANGED
@@ -1,7 +1,8 @@
1
  # config.py
2
 
3
- # Prompt para o primeiro agente (GROK)
4
- PROMPT_GROK = """
 
5
  <prompt>
6
  <role>
7
  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.
@@ -41,12 +42,12 @@ PROMPT_GROK = """
41
  5. Mantenha tom respeitoso e reflexivo ao longo do texto
42
  6. Organize o conteúdo de forma lógica e progressiva
43
  7. Evite usar um estilo de escrita muito característico de textos gerados com IA, como por exemplo: "Não é mera..., mas é...". Coisas assim. Seja mais direto.
 
44
  </instructions>
45
  </prompt>
46
  """
47
 
48
- # Prompt para o segundo agente (Claude Sonnet)
49
- PROMPT_CLAUDE_SONNET = """
50
  Com base na solicitação original do usuário e no texto gerado pelo primeiro especialista, sua tarefa é analisar criticamente o texto e aprimorá-lo. Não faça reduções e nem resumos. Se conseguir aprofundar e detalhar melhor o texto, adicionar novas referência de novos autores, faça. Se não conseguir, não faça nada.
51
 
52
  **Solicitação Original do Usuário:**
@@ -60,19 +61,19 @@ Com base na solicitação original do usuário e no texto gerado pelo primeiro e
60
  ---
61
 
62
  **Suas Instruções:**
63
- 1. **Valide se o texto atingiu a quantidade de palavras mínimas (4000 palavras).
64
  2. **Analise o texto:** Verifique a coesão, coerência e profundidade dos argumentos.
65
  3. **Aprofunde e Detalhe:** Identifique pontos que podem ser mais explorados. Adicione detalhes, exemplos e nuances que enriqueçam o conteúdo original.
66
  4. **Faça Correções:** Corrija eventuais imprecisões conceituais ou argumentativas.
67
  5. **Não Resuma ou Reduza:** Seu objetivo é expandir e aprofundar, nunca encurtar o texto. O resultado final deve ser uma versão mais completa e robusta do que a original.
68
  6. **Mantenha o Estilo:** Respeite o estilo de linguagem e o tom do texto original.
69
- 7. **Evite usar um estilo de escrita muito característico de textos gerados com IA, como por exemplo: "Não é mera..., mas é...". Coisas assim. Seja mais direto.
 
70
 
71
  Reescreva o texto completo, incorporando suas melhorias, detalhamentos e correções.
72
  """
73
 
74
- # Prompt para o terceiro agente (Gemini)
75
- PROMPT_GEMINI = """
76
  Você é o revisor final. Sua função é polir e aperfeiçoar o texto que já passou por uma primeira rodada de escrita e uma segunda de revisão e aprofundamento. Não faça reduções e nem resumos. Se conseguir aprofundar e detalhar melhor o texto, adicionar novas referência de novos autores, faça. Se não conseguir, não faça nada.
77
 
78
  **Solicitação Original do Usuário:**
@@ -87,12 +88,94 @@ Você é o revisor final. Sua função é polir e aperfeiçoar o texto que já p
87
 
88
  **Suas Instruções:**
89
  1. **Análise Crítica Final:** Leia o texto atentamente, buscando a máxima qualidade, clareza e profundidade.
90
- 2. **Valide se o texto atingiu a quantidade de palavras mínimas (4000 palavras).
91
  3. **Correções e Complementos Finais:** Adicione os toques finais. Melhore a fluidez entre os parágrafos, enriqueça o vocabulário e adicione insights que possam ter sido omitidos.
92
  4. **Não Resuma ou Reduza:** Assim como o revisor anterior, seu papel é adicionar valor e profundidade, não remover conteúdo.
93
  5. **Garantia de Qualidade:** Assegure que o texto final atende a todos os requisitos da solicitação original do usuário de forma exemplar.
94
- 6. **Exiba na resposta apenas o texto revisado, sem nenhuma outra mensagem para o usuário.
95
- 7. **Evite usar um estilo de escrita muito característico de textos gerados com IA, como por exemplo: "Não é mera..., mas é...". Coisas assim. Seja mais direto.
 
96
 
97
  Reescreva o texto completo com suas melhorias finais. O texto deve estar impecável e pronto para publicação.
98
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # config.py
2
 
3
+ # --- PROMPTS PARA O MODO HIERÁRQUICO ---
4
+
5
+ PROMPT_HIERARQUICO_GROK = """
6
  <prompt>
7
  <role>
8
  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.
 
42
  5. Mantenha tom respeitoso e reflexivo ao longo do texto
43
  6. Organize o conteúdo de forma lógica e progressiva
44
  7. Evite usar um estilo de escrita muito característico de textos gerados com IA, como por exemplo: "Não é mera..., mas é...". Coisas assim. Seja mais direto.
45
+ 8. Todo o texto, incluindo citações, devem estar na lingua Português do Brasil.
46
  </instructions>
47
  </prompt>
48
  """
49
 
50
+ PROMPT_HIERARQUICO_SONNET = """
 
51
  Com base na solicitação original do usuário e no texto gerado pelo primeiro especialista, sua tarefa é analisar criticamente o texto e aprimorá-lo. Não faça reduções e nem resumos. Se conseguir aprofundar e detalhar melhor o texto, adicionar novas referência de novos autores, faça. Se não conseguir, não faça nada.
52
 
53
  **Solicitação Original do Usuário:**
 
61
  ---
62
 
63
  **Suas Instruções:**
64
+ 1. **Valide se o texto atingiu a quantidade de palavras mínimas (4000 palavras)**.
65
  2. **Analise o texto:** Verifique a coesão, coerência e profundidade dos argumentos.
66
  3. **Aprofunde e Detalhe:** Identifique pontos que podem ser mais explorados. Adicione detalhes, exemplos e nuances que enriqueçam o conteúdo original.
67
  4. **Faça Correções:** Corrija eventuais imprecisões conceituais ou argumentativas.
68
  5. **Não Resuma ou Reduza:** Seu objetivo é expandir e aprofundar, nunca encurtar o texto. O resultado final deve ser uma versão mais completa e robusta do que a original.
69
  6. **Mantenha o Estilo:** Respeite o estilo de linguagem e o tom do texto original.
70
+ 7. **Evite usar um estilo de escrita muito característico de textos gerados com IA, como por exemplo: "Não é mera..., mas é...". Coisas assim. Seja mais direto.**
71
+ 8. **Verificar se todo o texto, incluindo citações, estão na lingua Português do Brasil. Traduza as que não estiverem.**
72
 
73
  Reescreva o texto completo, incorporando suas melhorias, detalhamentos e correções.
74
  """
75
 
76
+ PROMPT_HIERARQUICO_GEMINI = """
 
77
  Você é o revisor final. Sua função é polir e aperfeiçoar o texto que já passou por uma primeira rodada de escrita e uma segunda de revisão e aprofundamento. Não faça reduções e nem resumos. Se conseguir aprofundar e detalhar melhor o texto, adicionar novas referência de novos autores, faça. Se não conseguir, não faça nada.
78
 
79
  **Solicitação Original do Usuário:**
 
88
 
89
  **Suas Instruções:**
90
  1. **Análise Crítica Final:** Leia o texto atentamente, buscando a máxima qualidade, clareza e profundidade.
91
+ 2. **Valide se o texto atingiu a quantidade de palavras mínimas (4000 palavras).**
92
  3. **Correções e Complementos Finais:** Adicione os toques finais. Melhore a fluidez entre os parágrafos, enriqueça o vocabulário e adicione insights que possam ter sido omitidos.
93
  4. **Não Resuma ou Reduza:** Assim como o revisor anterior, seu papel é adicionar valor e profundidade, não remover conteúdo.
94
  5. **Garantia de Qualidade:** Assegure que o texto final atende a todos os requisitos da solicitação original do usuário de forma exemplar.
95
+ 6. **Exiba na resposta apenas o texto revisado, sem nenhuma outra mensagem para o usuário.**
96
+ 7. **Evite usar um estilo de escrita muito característico de textos gerados com IA, como por exemplo: "Não é mera..., mas é...". Coisas assim. Seja mais direto.**
97
+ 8. **Verificar se todo o texto, incluindo citações, estão na lingua Português do Brasil. Traduza as que não estiverem.**
98
 
99
  Reescreva o texto completo com suas melhorias finais. O texto deve estar impecável e pronto para publicação.
100
+ """
101
+
102
+
103
+ # --- PROMPTS PARA O MODO ATÔMICO ---
104
+
105
+ PROMPT_ATOMICO_INICIAL = """
106
+ <prompt>
107
+ <role>
108
+ 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.
109
+ </role>
110
+ <requirements>
111
+ <word_count>Entre 4000 e 5000 palavras</word_count>
112
+ <language>Português do Brasil</language>
113
+ <paragraph_structure>Parágrafos curtos para facilitar a leitura</paragraph_structure>
114
+ <language_style>
115
+ - Linguagem profunda e formal, mas acessível a leigos
116
+ - Evitar tecnicismos excessivos
117
+ - Evitar rigidez acadêmica desnecessária
118
+ - Manter profundidade intelectual sem perder clareza
119
+ </language_style>
120
+ </requirements>
121
+ <context_from_documents>
122
+ A seguir, trechos de documentos fornecidos pelo usuário para sua referência. Use-os como base teórica para enriquecer sua resposta.
123
+ ---
124
+ {rag_context}
125
+ ---
126
+ </context_from_documents>
127
+ <user_request>
128
+ <solicitacao_usuario>
129
+ {solicitacao_usuario}
130
+ </solicitacao_usuario>
131
+ </user_request>
132
+ <instructions>
133
+ Com base na solicitação do usuário acima, desenvolva um texto que:
134
+ 1. Explore o tema com profundidade filosófica e teológica
135
+ 2. Mantenha conexão com a tradição católica quando relevante
136
+ 3. Apresente argumentos bem estruturados e fundamentados
137
+ 4. Use exemplos práticos quando apropriado para ilustrar conceitos
138
+ 5. Mantenha tom respeitoso e reflexivo ao longo do texto
139
+ 6. Organize o conteúdo de forma lógica e progressiva
140
+ 7. Evite usar um estilo de escrita muito característico de textos gerados com IA, como por exemplo: "Não é mera..., mas é...". Coisas assim. Seja mais direto.
141
+ 8. Todo o texto, incluindo citações, devem estar na lingua Português do Brasil.
142
+ </instructions>
143
+ </prompt>
144
+ """
145
+
146
+ PROMPT_ATOMICO_MERGE = """
147
+ Com base na solicitação original do usuário e nos textos-base fornecidos, sua tarefa é analisar criticamente os textos e elaborar uma versão consolidada, unindo o que há de melhor em cada um deles.
148
+
149
+ **Solicitação Original do Usuário:**
150
+ ---
151
+ {solicitacao_usuario}
152
+ ---
153
+
154
+ **Texto Gerado pelo GROK:**
155
+ ---
156
+ {texto_para_analise_grok}
157
+ ---
158
+
159
+ **Texto Gerado pelo Sonnet:**
160
+ ---
161
+ {texto_para_analise_sonnet}
162
+ ---
163
+
164
+ **Texto Gerado pelo Gemini:**
165
+ ---
166
+ {texto_para_analise_gemini}
167
+ ---
168
+
169
+ **Suas Instruções:**
170
+ - **Estrutura:** Analise e escolha a melhor estrutura de seções entre os 3 textos e aplique no texto consolidado. A melhor estrutura de seções é aquela que melhor entendeu o objetivo da solicitação do usuário e que mais conseguir se aprofundar na abordagem do tema.
171
+ - **Valide se o texto atingiu a quantidade de palavras mínimas (4000 palavras).
172
+ - **Analise o texto:** Verifique a coesão, coerência e profundidade dos argumentos.
173
+ - **Consolidação:** Identifique os pontos fortes de cada texto e gere um texto final consolidado. Cuide para o que texto não fique redundante. Ou seja, voltando nos mesmos assuntos e conceitos.
174
+ - **Faça Correções:** Corrija eventuais imprecisões conceituais ou argumentativas.
175
+ - **Não Resuma ou Reduza:** Seu objetivo é expandir e aprofundar, nunca encurtar o texto. O resultado final deve ser uma versão mais completa e robusta do que a original.
176
+ - **Mantenha o Estilo:** Respeite o estilo de linguagem e o tom do texto original.
177
+ - **Evite usar um estilo de escrita muito característico de textos gerados com IA, como por exemplo: "Não é mera..., mas é...". Coisas assim. Seja mais direto.
178
+ - **Verificar se todo o texto, incluindo citações, estão na lingua Português do Brasil. Traduza as que não estiverem.
179
+
180
+ Reescreva o texto completo, incorporando suas melhorias, detalhamentos e correções.
181
+ """
static/style.css CHANGED
@@ -362,3 +362,50 @@ textarea.drag-over {
362
  .remove-file-btn:hover {
363
  color: #a02430;
364
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
  .remove-file-btn:hover {
363
  color: #a02430;
364
  }
365
+
366
+ /* static/style.css (ADICIONAR NO FINAL) */
367
+
368
+ /* Botão Flutuante de Merge */
369
+ .floating-merge-btn {
370
+ position: fixed;
371
+ bottom: 30px;
372
+ right: 30px;
373
+ z-index: 999;
374
+ padding: 15px 25px;
375
+ font-size: 18px;
376
+ font-weight: bold;
377
+ background-color: #ffc107; /* Amarelo/Laranja */
378
+ color: #212529;
379
+ border: none;
380
+ border-radius: 50px;
381
+ cursor: pointer;
382
+ box-shadow: 0 4px 10px rgba(0,0,0,0.2);
383
+ transition: all 0.3s ease;
384
+ }
385
+
386
+ .floating-merge-btn:hover {
387
+ transform: translateY(-3px);
388
+ box-shadow: 0 6px 15px rgba(0,0,0,0.3);
389
+ }
390
+
391
+ /* Container do Resultado Final */
392
+ #final-result-container {
393
+ margin: 40px auto 0 auto;
394
+ padding: 20px;
395
+ border: 1px solid #ddd;
396
+ border-radius: 8px;
397
+ background-color: #f8f9fa;
398
+ width: 60%;
399
+ }
400
+
401
+ #final-result-container h2 {
402
+ text-align: center;
403
+ margin-top: 0;
404
+ }
405
+
406
+ /* Responsividade para o container final em telas menores */
407
+ @media (max-width: 768px) {
408
+ #final-result-container {
409
+ width: 90%;
410
+ }
411
+ }
templates/index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
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>
@@ -12,19 +12,27 @@
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">
16
- <div id="progress-bar" class="progress-bar"></div>
17
- </div>
18
  </div>
19
  </div>
20
 
 
 
21
  <div class="container">
22
  <div class="header-container">
23
  <div>
24
  <h1>Sistema Multi-Agente IA</h1>
25
- <p>GROK ➔ Claude Sonnet ➔ Gemini</p>
26
  </div>
27
  <div class="controls-container">
 
 
 
 
 
 
 
 
28
  <div class="mode-toggle">
29
  <span>Modo Real</span>
30
  <label class="switch"><input type="checkbox" id="mode-switch"><span class="slider round"></span></label>
@@ -44,7 +52,6 @@
44
  <button type="submit">Processar com IA</button>
45
  </form>
46
  </div>
47
-
48
  <div id="mock-form-container" style="display: none;">
49
  <form id="request-form-mock">
50
  <label for="mock_text">Cole o texto de simulação aqui:</label>
@@ -54,24 +61,20 @@
54
  </div>
55
 
56
  <div id="results-container" class="results-container" style="display: none;">
57
- <div class="result-column">
58
- <div class="column-header"><h2>GROK</h2><button class="copy-btn" onclick="copyToClipboard('grok-output')">Copiar</button></div>
59
- <div class="output-box" id="grok-output"></div>
60
- </div>
61
- <div class="result-column">
62
- <div class="column-header"><h2>Claude Sonnet</h2><button class="copy-btn" onclick="copyToClipboard('sonnet-output')">Copiar</button></div>
63
- <div class="output-box" id="sonnet-output"></div>
64
- </div>
65
- <div class="result-column">
66
- <div class="column-header"><h2>Gemini</h2><button class="copy-btn" onclick="copyToClipboard('gemini-output')">Copiar</button></div>
67
- <div class="output-box" id="gemini-output"></div>
68
- </div>
69
  </div>
70
  </div>
71
 
72
  <script>
73
- // O JavaScript continua o mesmo, pois ele preenche os resultados
74
- // pelos IDs, que não mudaram. Nenhuma alteração é necessária aqui.
75
  const modeSwitch = document.getElementById('mode-switch');
76
  const realContainer = document.getElementById('real-form-container');
77
  const mockContainer = document.getElementById('mock-form-container');
@@ -82,13 +85,24 @@
82
  const errorContainer = document.getElementById('error-box-container');
83
  const textarea = document.getElementById('solicitacao_usuario');
84
  const fileList = document.getElementById('file-list');
 
 
 
85
  let attachedFiles = [];
 
86
 
 
87
  modeSwitch.addEventListener('change', function() {
88
  realContainer.style.display = this.checked ? 'none' : 'block';
89
  mockContainer.style.display = this.checked ? 'block' : 'none';
90
  });
91
 
 
 
 
 
 
 
92
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
93
  textarea.addEventListener(eventName, preventDefaults, false);
94
  document.body.addEventListener(eventName, preventDefaults, false);
@@ -100,26 +114,17 @@
100
  textarea.addEventListener(eventName, () => textarea.classList.remove('drag-over'), false);
101
  });
102
  textarea.addEventListener('drop', handleDrop, false);
103
-
104
  function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
105
  function handleDrop(e) { handleFiles(e.dataTransfer.files); }
106
-
107
  function handleFiles(files) {
108
  [...files].forEach(file => {
109
  const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'];
110
- if (!allowedTypes.includes(file.type)) {
111
- showError(`Formato de arquivo não suportado: ${file.name}`);
112
- return;
113
- }
114
- if (file.size > 100 * 1024 * 1024) { // 100MB
115
- showError(`Arquivo muito grande (max 100MB): ${file.name}`);
116
- return;
117
- }
118
  attachedFiles.push(file);
119
  });
120
  updateFileList();
121
  }
122
-
123
  function updateFileList() {
124
  fileList.innerHTML = '';
125
  document.getElementById('file-list-container').style.display = attachedFiles.length > 0 ? 'block' : 'none';
@@ -129,61 +134,57 @@
129
  const removeBtn = document.createElement('span');
130
  removeBtn.textContent = '×';
131
  removeBtn.className = 'remove-file-btn';
132
- removeBtn.onclick = () => {
133
- attachedFiles.splice(index, 1);
134
- updateFileList();
135
- };
136
  li.appendChild(removeBtn);
137
  fileList.appendChild(li);
138
  });
139
  }
140
 
 
141
  document.getElementById('request-form-real').addEventListener('submit', handleFormSubmit);
142
  document.getElementById('request-form-mock').addEventListener('submit', handleFormSubmit);
143
 
144
  async function handleFormSubmit(event) {
145
  event.preventDefault();
146
 
 
147
  errorContainer.innerHTML = '';
148
  resultsContainer.style.display = 'none';
 
 
149
  document.querySelectorAll('.output-box').forEach(box => box.innerHTML = '');
150
 
 
151
  loaderMessage.textContent = 'Iniciando conexão...';
152
  progressBar.style.width = '0%';
153
  loader.style.display = 'flex';
154
 
155
  const formData = new FormData();
 
 
156
  if (modeSwitch.checked) {
157
  formData.append('mode', 'test');
158
  formData.append('mock_text', document.getElementById('mock_text').value);
 
159
  } else {
160
  formData.append('mode', 'real');
161
- formData.append('solicitacao', document.getElementById('solicitacao_usuario').value);
162
- attachedFiles.forEach(file => {
163
- formData.append('files', file);
164
- });
165
  }
166
 
 
 
167
  try {
168
- const response = await fetch('/process', {
169
- method: 'POST',
170
- body: formData,
171
- });
172
-
173
- if (!response.ok || !response.body) {
174
- throw new Error(`Erro na resposta do servidor: ${response.statusText}`);
175
- }
176
-
177
  const reader = response.body.getReader();
178
  const decoder = new TextDecoder();
179
-
180
  while (true) {
181
  const { done, value } = await reader.read();
182
  if (done) break;
183
-
184
  const chunk = decoder.decode(value, { stream: true });
185
  const lines = chunk.split('\n\n');
186
-
187
  lines.forEach(line => {
188
  if (line.startsWith('data: ')) {
189
  const jsonData = line.substring(6);
@@ -191,15 +192,13 @@
191
  try {
192
  const data = JSON.parse(jsonData);
193
  processStreamData(data);
194
- } catch (e) {
195
- console.error("Erro ao parsear JSON do stream:", jsonData);
196
- }
197
  }
198
  }
199
  });
200
  }
201
  } catch (error) {
202
- showError('A conexão com o servidor falhou. Verifique o console para detalhes.');
203
  loader.style.display = 'none';
204
  console.error("Fetch Error:", error);
205
  }
@@ -218,27 +217,62 @@
218
  if (data.partial_result) {
219
  resultsContainer.style.display = 'flex';
220
  const targetBox = document.getElementById(data.partial_result.id);
221
- if (targetBox) {
222
- targetBox.innerHTML = data.partial_result.content;
223
- }
224
  }
225
 
226
  if (data.done) {
227
  setTimeout(() => {
228
  loader.style.display = 'none';
 
 
 
 
229
  }, 1000);
230
  }
231
  }
232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  function showError(message) {
234
  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>`;
235
  }
236
 
237
  function copyToClipboard(elementId) {
238
  const element = document.getElementById(elementId);
239
- navigator.clipboard.writeText(element.innerText).then(() => {
240
- alert('Texto copiado!');
241
- });
242
  }
243
  </script>
244
  </body>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Sistema Multi-Agente IA</title>
7
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
8
  </head>
9
  <body>
 
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">
31
+ <input type="checkbox" id="processing-mode-switch">
32
+ <span class="slider round"></span>
33
+ </label>
34
+ <span>Atômico</span>
35
+ </div>
36
  <div class="mode-toggle">
37
  <span>Modo Real</span>
38
  <label class="switch"><input type="checkbox" id="mode-switch"><span class="slider round"></span></label>
 
52
  <button type="submit">Processar com IA</button>
53
  </form>
54
  </div>
 
55
  <div id="mock-form-container" style="display: none;">
56
  <form id="request-form-mock">
57
  <label for="mock_text">Cole o texto de simulação aqui:</label>
 
61
  </div>
62
 
63
  <div id="results-container" class="results-container" style="display: none;">
64
+ <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>
65
+ <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>
66
+ <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>
67
+ </div>
68
+
69
+ <div id="final-result-container" style="display: none;">
70
+ <h2>Texto Final</h2>
71
+ <div class="output-box" id="final-output"></div>
 
 
 
 
72
  </div>
73
  </div>
74
 
75
  <script>
76
+ // --- Variáveis Globais ---
77
+ const processingModeSwitch = document.getElementById('processing-mode-switch');
78
  const modeSwitch = document.getElementById('mode-switch');
79
  const realContainer = document.getElementById('real-form-container');
80
  const mockContainer = document.getElementById('mock-form-container');
 
85
  const errorContainer = document.getElementById('error-box-container');
86
  const textarea = document.getElementById('solicitacao_usuario');
87
  const fileList = document.getElementById('file-list');
88
+ const mergeBtn = document.getElementById('merge-btn');
89
+ const finalResultContainer = document.getElementById('final-result-container');
90
+ const finalOutput = document.getElementById('final-output');
91
  let attachedFiles = [];
92
+ let originalUserQuery = ""; // Salvar a query original
93
 
94
+ // --- Lógica de UI ---
95
  modeSwitch.addEventListener('change', function() {
96
  realContainer.style.display = this.checked ? 'none' : 'block';
97
  mockContainer.style.display = this.checked ? 'block' : 'none';
98
  });
99
 
100
+ processingModeSwitch.addEventListener('change', function() {
101
+ const description = document.getElementById('flow-description');
102
+ description.textContent = this.checked ? "GROK | Claude Sonnet | Gemini (Paralelo)" : "GROK ➔ Claude Sonnet ➔ Gemini";
103
+ });
104
+
105
+ // --- Lógica de Upload (sem alterações) ---
106
  ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
107
  textarea.addEventListener(eventName, preventDefaults, false);
108
  document.body.addEventListener(eventName, preventDefaults, false);
 
114
  textarea.addEventListener(eventName, () => textarea.classList.remove('drag-over'), false);
115
  });
116
  textarea.addEventListener('drop', handleDrop, false);
 
117
  function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
118
  function handleDrop(e) { handleFiles(e.dataTransfer.files); }
 
119
  function handleFiles(files) {
120
  [...files].forEach(file => {
121
  const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'];
122
+ if (!allowedTypes.includes(file.type)) { showError(`Formato não suportado: ${file.name}`); return; }
123
+ if (file.size > 100 * 1024 * 1024) { showError(`Arquivo muito grande: ${file.name}`); return; }
 
 
 
 
 
 
124
  attachedFiles.push(file);
125
  });
126
  updateFileList();
127
  }
 
128
  function updateFileList() {
129
  fileList.innerHTML = '';
130
  document.getElementById('file-list-container').style.display = attachedFiles.length > 0 ? 'block' : 'none';
 
134
  const removeBtn = document.createElement('span');
135
  removeBtn.textContent = '×';
136
  removeBtn.className = 'remove-file-btn';
137
+ removeBtn.onclick = () => { attachedFiles.splice(index, 1); updateFileList(); };
 
 
 
138
  li.appendChild(removeBtn);
139
  fileList.appendChild(li);
140
  });
141
  }
142
 
143
+ // --- Lógica de Submissão Principal ---
144
  document.getElementById('request-form-real').addEventListener('submit', handleFormSubmit);
145
  document.getElementById('request-form-mock').addEventListener('submit', handleFormSubmit);
146
 
147
  async function handleFormSubmit(event) {
148
  event.preventDefault();
149
 
150
+ // Limpa o estado da UI
151
  errorContainer.innerHTML = '';
152
  resultsContainer.style.display = 'none';
153
+ finalResultContainer.style.display = 'none';
154
+ mergeBtn.style.display = 'none';
155
  document.querySelectorAll('.output-box').forEach(box => box.innerHTML = '');
156
 
157
+ // Inicia o loader
158
  loaderMessage.textContent = 'Iniciando conexão...';
159
  progressBar.style.width = '0%';
160
  loader.style.display = 'flex';
161
 
162
  const formData = new FormData();
163
+ formData.append('processing_mode', processingModeSwitch.checked ? 'atomic' : 'hierarchical');
164
+
165
  if (modeSwitch.checked) {
166
  formData.append('mode', 'test');
167
  formData.append('mock_text', document.getElementById('mock_text').value);
168
+ originalUserQuery = "Simulação de teste.";
169
  } else {
170
  formData.append('mode', 'real');
171
+ originalUserQuery = document.getElementById('solicitacao_usuario').value;
172
+ formData.append('solicitacao', originalUserQuery);
173
+ attachedFiles.forEach(file => { formData.append('files', file); });
 
174
  }
175
 
176
+ // A lógica de streaming via fetch continua a mesma
177
+ // ... (código do fetch e do reader, sem alterações)
178
  try {
179
+ const response = await fetch('/process', { method: 'POST', body: formData });
180
+ if (!response.ok || !response.body) throw new Error(`Erro na resposta do servidor: ${response.statusText}`);
 
 
 
 
 
 
 
181
  const reader = response.body.getReader();
182
  const decoder = new TextDecoder();
 
183
  while (true) {
184
  const { done, value } = await reader.read();
185
  if (done) break;
 
186
  const chunk = decoder.decode(value, { stream: true });
187
  const lines = chunk.split('\n\n');
 
188
  lines.forEach(line => {
189
  if (line.startsWith('data: ')) {
190
  const jsonData = line.substring(6);
 
192
  try {
193
  const data = JSON.parse(jsonData);
194
  processStreamData(data);
195
+ } catch (e) { console.error("Erro ao parsear JSON:", jsonData); }
 
 
196
  }
197
  }
198
  });
199
  }
200
  } catch (error) {
201
+ showError('A conexão com o servidor falhou.');
202
  loader.style.display = 'none';
203
  console.error("Fetch Error:", error);
204
  }
 
217
  if (data.partial_result) {
218
  resultsContainer.style.display = 'flex';
219
  const targetBox = document.getElementById(data.partial_result.id);
220
+ if (targetBox) targetBox.innerHTML = data.partial_result.content;
 
 
221
  }
222
 
223
  if (data.done) {
224
  setTimeout(() => {
225
  loader.style.display = 'none';
226
+ // Mostra o botão de merge apenas se o modo for Atômico
227
+ if (data.mode === 'atomic') {
228
+ mergeBtn.style.display = 'block';
229
+ }
230
  }, 1000);
231
  }
232
  }
233
 
234
+ // --- Lógica do Botão de Merge ---
235
+ mergeBtn.addEventListener('click', async function() {
236
+ loaderMessage.textContent = 'Processando o merge dos textos...';
237
+ progressBar.style.width = '0%';
238
+ loader.style.display = 'flex';
239
+ this.style.display = 'none'; // Esconde o botão
240
+
241
+ const payload = {
242
+ solicitacao_usuario: originalUserQuery,
243
+ grok_text: document.getElementById('grok-output').innerText,
244
+ sonnet_text: document.getElementById('sonnet-output').innerText,
245
+ gemini_text: document.getElementById('gemini-output').innerText,
246
+ };
247
+
248
+ try {
249
+ const response = await fetch('/merge', {
250
+ method: 'POST',
251
+ headers: { 'Content-Type': 'application/json' },
252
+ body: JSON.stringify(payload)
253
+ });
254
+ const result = await response.json();
255
+
256
+ if (result.success) {
257
+ finalOutput.innerHTML = result.content;
258
+ finalResultContainer.style.display = 'block';
259
+ } else {
260
+ showError(result.error || "Ocorreu um erro desconhecido no merge.");
261
+ }
262
+ } catch (error) {
263
+ showError("A conexão falhou ao tentar processar o merge.");
264
+ } finally {
265
+ loader.style.display = 'none';
266
+ }
267
+ });
268
+
269
  function showError(message) {
270
  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>`;
271
  }
272
 
273
  function copyToClipboard(elementId) {
274
  const element = document.getElementById(elementId);
275
+ navigator.clipboard.writeText(element.innerText).then(() => { alert('Texto copiado!'); });
 
 
276
  }
277
  </script>
278
  </body>