88import json
99import re
1010from collections import Counter
11- import itertools # CORRIGIDO: importação duplicada removida
11+ import itertools # importação duplicada removida
1212import streamlit .components .v1 as components
1313from streamlit_agraph import agraph , Node , Edge , Config
1414import numpy as np
4646)
4747
4848# --- INICIALIZAÇÃO DEFENSIVA DE ESTADO ---
49- # CORRIGIDO: sna_global, df_grid e tipo_foresight_grid adicionados ao bloco
49+ # sna_global, df_grid e tipo_foresight_grid adicionados ao bloco
5050# defensivo para evitar recálculos custosos e perda de resultados no rerender.
5151chaves_necessarias = {
5252 'grafo_pronto' : False ,
205205 st .markdown ("### 🧬 Métricas de Ecologia Profunda (SNA Avançado)" )
206206 st .caption ("Diagnóstico estrutural e físico da maturidade do ecossistema de conhecimento." )
207207
208- # CORRIGIDO: sna_global agora é cacheado em session_state para evitar
208+ # sna_global agora é cacheado em session_state para evitar
209209 # recálculo duplo quando o usuário navega para a aba Fluxos/Foresight.
210210 if st .session_state ['sna_global' ] is None :
211211 with st .spinner ("Calculando leis de escala e correlações topológicas..." ):
290290 linhas_tabela = []
291291 macrotemas_unicos = set ([d .get ('macrotema' , 'Multidisciplinar / Transversal' ) for d in dados_completos ])
292292
293- # CORRIGIDO: top_ql movida para fora do loop. Defini-la dentro do loop
293+ # top_ql movida para fora do loop. Defini-la dentro do loop
294294 # a redefinia desnecessariamente a cada iteração (antipattern Python).
295295 def top_ql (entidades_na_mt , contagem_global , O_k , O_total ):
296296 max_ql = - 1
@@ -745,7 +745,7 @@ def top_ql(entidades_na_mt, contagem_global, O_k, O_total):
745745
746746 df_bt = calcular_burt (dados_gerais )
747747 if not df_bt .empty :
748- # CORRIGIDO: template alterado de "plotly_dark" para "streamlit" para
748+ # template alterado de "plotly_dark" para "streamlit" para
749749 # suportar os modos claro e escuro da interface de forma consistente
750750 # com o restante dos gráficos do arquivo.
751751 st .plotly_chart (
@@ -947,16 +947,27 @@ def top_ql(entidades_na_mt, contagem_global, O_k, O_total):
947947
948948 col_for1 , col_for2 = st .columns (2 )
949949 with col_for1 :
950- tipo_foresight = st .selectbox ("Analisar Emergência de:" , ["Palavra-chave" , "Macrotema" ])
950+ tipo_foresight = st .selectbox ("Analisar Emergência de:" , ["Palavra-chave" , "Macrotema" , "Artefatos (Ontologia IA)" ])
951951 with col_for2 :
952952 janela_anos = st .slider ("Janela Recente (Últimos X anos para medir):" , min_value = 1 , max_value = 10 , value = 3 )
953953
954- # CÓDIGO NOVO — com controle de bootstrap
955- sna_global = st .session_state ['sna_global' ]
956- if sna_global is None :
957- with st .spinner ("Sincronizando topologia para o Radar..." ):
958- sna_global = calcular_sna_global (dados_gerais )
959- st .session_state ['sna_global' ] = sna_global
954+ # --- VERIFICAÇÃO DE SEGURANÇA DA ONTOLOGIA ---
955+ tem_ontologia = True
956+ if tipo_foresight == "Artefatos (Ontologia IA)" :
957+ # Verifica se pelo menos 1 documento na base possui os dados da IA
958+ tem_ontologia = any ('ontologia_ia' in d and d ['ontologia_ia' ] for d in dados_gerais )
959+ if not tem_ontologia :
960+ st .warning ("⚠️ O catálogo ontológico ainda não foi gerado nesta sessão." )
961+ st .info ("Para analisar a emergência de Artefatos, vá até a aba **'🧬 Memética'** e processe um lote com o Gemini ou faça o upload do seu CSV." )
962+
963+ # Indentamos o resto do Radar para só rodar se passar na verificação
964+ if tem_ontologia :
965+ # Com controle de bootstrap
966+ sna_global = st .session_state ['sna_global' ]
967+ if sna_global is None :
968+ with st .spinner ("Sincronizando topologia para o Radar..." ):
969+ sna_global = calcular_sna_global (dados_gerais )
970+ st .session_state ['sna_global' ] = sna_global
960971
961972 # Controle de robustez estatística (3ª coluna no layout)
962973 col_for3 = st .columns (1 )[0 ]
@@ -992,7 +1003,7 @@ def top_ql(entidades_na_mt, contagem_global, O_k, O_total):
9921003 bet_bootstrap = bet_bootstrap # NOVO
9931004 )
9941005
995- # CORRIGIDO: guard substituído de .sum() == 0 (frágil para floats próximos
1006+ # guard substituído de .sum() == 0 (frágil para floats próximos
9961007 # de zero por imprecisão numérica) para .max() < 1e-9 (numericamente robusto).
9971008 if df_foresight .empty or df_foresight ['Novidade (Estrutural * IDF)' ].max () < 1e-9 :
9981009 st .info ("Não há dados recentes ou de rede suficientes para plotar o Radar de Prospecção." )
@@ -1083,12 +1094,12 @@ def top_ql(entidades_na_mt, contagem_global, O_k, O_total):
10831094 x_mid = mom_mediano
10841095 y_mid = nov_mediana
10851096
1086- # CORRIGIDO: hover_data agora é construído dinamicamente para evitar
1097+ # hover_data construído dinamicamente para evitar
10871098 # ValueError se o backend retornar um dataframe sem essas colunas.
10881099 colunas_hover_desejadas = ["Tração (%)" , "Aparições Recentes" ]
10891100 hover_cols = [c for c in colunas_hover_desejadas if c in df_foresight .columns ]
10901101
1091- # CORRIGIDO: template alterado de "plotly_dark" para "streamlit" para
1102+ # template alterado de "plotly_dark" para "streamlit" para
10921103 # consistência com o tema da aplicação (suporte a light/dark mode).
10931104 fig_radar = px .scatter (
10941105 df_foresight ,
@@ -1111,7 +1122,7 @@ def top_ql(entidades_na_mt, contagem_global, O_k, O_total):
11111122 fig_radar .add_vline (x = x_mid , line_dash = "dash" , line_color = "#7F8C8D" , line_width = 1 )
11121123 fig_radar .add_hline (y = y_mid , line_dash = "dash" , line_color = "#7F8C8D" , line_width = 1 )
11131124
1114- # CORRIGIDO: anotações migradas para coordenadas de paper (0.0–1.0)
1125+ # anotações migradas para coordenadas de paper (0.0–1.0)
11151126 # em vez de coordenadas de dados (x_max, y_min, etc.). Isso garante
11161127 # que os rótulos fiquem sempre nos cantos do gráfico, sem sobrepor
11171128 # os pontos de dados mais extremos — que são exatamente os mais importantes.
@@ -1161,7 +1172,7 @@ def top_ql(entidades_na_mt, contagem_global, O_k, O_total):
11611172 """ )
11621173
11631174 if st .button ("🚀 Executar Bateria de Testes (Auto-Otimização)" , type = "primary" ):
1164- # CORRIGIDO: mensagens de progresso movidas para dentro de um callback
1175+ # mensagens de progresso movidas para dentro de um callback
11651176 # sequencial com st.empty() para refletir o progresso real da operação,
11661177 # em vez de exibir todos os passos concluídos antes do processamento.
11671178 _status_placeholder = st .empty ()
@@ -1173,12 +1184,12 @@ def top_ql(entidades_na_mt, contagem_global, O_k, O_total):
11731184 _status_placeholder .error ("❌ Dados insuficientes para rodar o Grid Search no histórico." )
11741185 else :
11751186 _status_placeholder .success ("✅ Bateria de Testes Concluída!" )
1176- # CORRIGIDO: resultado e tipo utilizado persistidos em session_state
1187+ # resultado e tipo utilizado persistidos em session_state
11771188 # para sobreviver à navegação entre abas e rerenders do Streamlit.
11781189 st .session_state ['df_grid' ] = df_grid_resultado
11791190 st .session_state ['tipo_foresight_grid' ] = tipo_foresight
11801191
1181- # CORRIGIDO: renderização dos resultados movida para fora do bloco do botão.
1192+ # renderização dos resultados movida para fora do bloco do botão.
11821193 # Assim os resultados permanecem visíveis após qualquer rerender, enquanto o
11831194 # usuário não acionar um novo Grid Search.
11841195 if st .session_state .get ('df_grid' ) is not None and not st .session_state ['df_grid' ].empty :
@@ -1262,10 +1273,57 @@ def highlight_first(s):
12621273
12631274 st .markdown ("---" )
12641275
1265- # --- TABELA DE VISUALIZAÇÃO E EXPORTAÇÃO DA IA (BLINDADA) ---
1276+ # --- TABELA DE VISUALIZAÇÃO, UPLOAD E EXPORTAÇÃO DA IA ---
12661277 st .markdown ("#### 🗂️ Catálogo de Artefatos Extraídos (Base de Conhecimento)" )
1267- st .caption ("Verifique e baixe o que a inteligência artificial conseguiu encontrar nos documentos processados até o momento." )
1278+ st .caption ("Verifique e baixe o que a inteligência artificial encontrou, ou faça upload de um catálogo pré-processado." )
1279+
1280+ # --- MÓDULO DE UPLOAD DE CSV ---
1281+ with st .expander ("⬆️ Fazer Upload de Catálogo Existente (CSV)" ):
1282+ st .markdown ("Já possui um catálogo processado anteriormente? Faça o upload do arquivo CSV para injetar os dados diretamente na base atual sem gastar cotas da API." )
1283+ arquivo_up = st .file_uploader ("Selecione o arquivo CSV do Catálogo Ontológico" , type = ["csv" ])
1284+
1285+ if arquivo_up is not None :
1286+ try :
1287+ df_up = pd .read_csv (arquivo_up )
1288+ colunas_necessarias = ['Título' , 'Teorias e Modelos' , 'Ferramentas e Artefatos' , 'Métodos e Técnicas' ]
1289+
1290+ if all (col in df_up .columns for col in colunas_necessarias ):
1291+ with st .spinner ("Injetando dados na rede..." ):
1292+ docs_atualizados = 0
1293+ # Converte a base atual para acesso rápido por título
1294+ mapa_titulos = {str (d .get ('titulo' , '' )).strip ().lower (): d for d in dados_gerais }
1295+
1296+ for index , row in df_up .iterrows ():
1297+ titulo_csv = str (row ['Título' ]).strip ().lower ()
1298+ if titulo_csv in mapa_titulos :
1299+ d = mapa_titulos [titulo_csv ]
1300+
1301+ # Função auxiliar para reverter a string do CSV (separada por vírgula) para Lista
1302+ def parse_str_to_list (s ):
1303+ if pd .isna (s ) or str (s ).strip () == "" : return []
1304+ return [item .strip () for item in str (s ).split ("," )]
1305+
1306+ # Atualiza o documento com a ontologia lida do CSV
1307+ d ['ontologia_ia' ] = {
1308+ 'teorias_e_modelos' : parse_str_to_list (row ['Teorias e Modelos' ]),
1309+ 'ferramentas_e_artefatos' : parse_str_to_list (row ['Ferramentas e Artefatos' ]),
1310+ 'metodos_e_tecnicas' : parse_str_to_list (row ['Métodos e Técnicas' ])
1311+ }
1312+ docs_atualizados += 1
1313+
1314+ if docs_atualizados > 0 :
1315+ st .session_state ['dados_completos' ] = dados_gerais
1316+ st .success (f"✅ Sucesso! { docs_atualizados } documentos foram enriquecidos com o catálogo carregado." )
1317+ time .sleep (2 )
1318+ st .rerun ()
1319+ else :
1320+ st .warning ("⚠️ O upload foi lido, mas nenhum 'Título' do CSV coincidiu com a base atual que está carregada na memória." )
1321+ else :
1322+ st .error ("O CSV não possui as colunas estruturais corretas. Certifique-se de usar o arquivo gerado pelo botão 'Exportar Catálogo' desta plataforma." )
1323+ except Exception as e :
1324+ st .error (f"Erro ao ler o arquivo CSV: { e } " )
12681325
1326+ # Lógica de renderização da tabela atual
12691327 dados_ontologia = []
12701328 for d in dados_gerais :
12711329 if 'ontologia_ia' in d and d ['ontologia_ia' ]:
@@ -1322,7 +1380,7 @@ def safe_join(item):
13221380
13231381 fonte_param = "Artefatos Extraídos" if "IA" in fonte_selecionada else "Palavras-chave"
13241382
1325- # CORRIGIDO: dupla chamada idêntica de calcular_metricas_memeticas removida.
1383+ # dupla chamada idêntica de calcular_metricas_memeticas removida.
13261384 # A primeira chamada (linhas 1151-1153 originais) era imediatamente sobrescrita
13271385 # pela segunda, causando processamento duplo desnecessário.
13281386 df_fecundidade , df_longevidade , mortos , vivos , df_mortos , df_vivos = calcular_metricas_memeticas (
0 commit comments