Guilherme Silberfarb Costa commited on
Commit
275fd5b
·
1 Parent(s): e018b1c

inclusao do botao salvar no repositorio e do campo de obsercaoes

Browse files
backend/app/api/elaboracao.py CHANGED
@@ -144,6 +144,11 @@ class AvaliacaoBasePayload(SessionPayload):
144
  class ExportModeloPayload(SessionPayload):
145
  nome_arquivo: str
146
  elaborador: dict[str, Any] | None = None
 
 
 
 
 
147
 
148
 
149
  class UpdateMapaPayload(SessionPayload):
@@ -455,7 +460,12 @@ def listar_avaliadores() -> dict[str, Any]:
455
  @router.post("/export-model")
456
  def export_model(payload: ExportModeloPayload, request: Request) -> FileResponse:
457
  session = session_store.get(payload.session_id)
458
- caminho, _ = elaboracao_service.exportar_modelo(session, payload.nome_arquivo, elaborador=payload.elaborador)
 
 
 
 
 
459
  user = auth_service.require_user(request)
460
  log_event(
461
  "elaboracao",
@@ -472,6 +482,33 @@ def export_model(payload: ExportModeloPayload, request: Request) -> FileResponse
472
  )
473
 
474
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
  @router.get("/export-base")
476
  def export_base(session_id: str, filtered: bool = True) -> FileResponse:
477
  session = session_store.get(session_id)
 
144
  class ExportModeloPayload(SessionPayload):
145
  nome_arquivo: str
146
  elaborador: dict[str, Any] | None = None
147
+ observacao_modelo: str | None = None
148
+
149
+
150
+ class SalvarModeloRepositorioPayload(ExportModeloPayload):
151
+ confirmar_substituicao: bool = False
152
 
153
 
154
  class UpdateMapaPayload(SessionPayload):
 
460
  @router.post("/export-model")
461
  def export_model(payload: ExportModeloPayload, request: Request) -> FileResponse:
462
  session = session_store.get(payload.session_id)
463
+ caminho, _ = elaboracao_service.exportar_modelo(
464
+ session,
465
+ payload.nome_arquivo,
466
+ elaborador=payload.elaborador,
467
+ observacao_modelo=payload.observacao_modelo,
468
+ )
469
  user = auth_service.require_user(request)
470
  log_event(
471
  "elaboracao",
 
482
  )
483
 
484
 
485
+ @router.post("/save-model-repository")
486
+ def save_model_repository(payload: SalvarModeloRepositorioPayload, request: Request) -> dict[str, Any]:
487
+ session = session_store.get(payload.session_id)
488
+ user = auth_service.require_admin(request)
489
+ resposta = elaboracao_service.salvar_modelo_repositorio(
490
+ session,
491
+ payload.nome_arquivo,
492
+ elaborador=payload.elaborador,
493
+ observacao_modelo=payload.observacao_modelo,
494
+ actor=user.get("usuario"),
495
+ confirmar_substituicao=payload.confirmar_substituicao,
496
+ )
497
+ log_event(
498
+ "repositorio",
499
+ "salvar_modelo_repositorio_elaboracao",
500
+ user=user,
501
+ session_id=payload.session_id,
502
+ request=request,
503
+ details={
504
+ "nome_arquivo": payload.nome_arquivo,
505
+ "confirmar_substituicao": bool(payload.confirmar_substituicao),
506
+ "substituidos": resposta.get("resultado_upload", {}).get("substituidos", []),
507
+ },
508
+ )
509
+ return resposta
510
+
511
+
512
  @router.get("/export-base")
513
  def export_base(session_id: str, filtered: bool = True) -> FileResponse:
514
  session = session_store.get(session_id)
backend/app/core/elaboracao/carregamento.py CHANGED
@@ -270,6 +270,7 @@ def carregar_dados_de_dai(caminho_arquivo):
270
  msg,
271
  sucesso,
272
  elaborador,
 
273
  outliers_excluidos,
274
  _periodo_dados_mercado,
275
  _config_avaliacao,
 
270
  msg,
271
  sucesso,
272
  elaborador,
273
+ _observacao_modelo,
274
  outliers_excluidos,
275
  _periodo_dados_mercado,
276
  _config_avaliacao,
backend/app/core/elaboracao/core.py CHANGED
@@ -376,12 +376,18 @@ def normalizar_config_avaliacao_modelo(config, df=None):
376
  }
377
 
378
 
 
 
 
 
 
 
379
  def carregar_dai(caminho):
380
  """
381
  Carrega arquivo .dai e extrai DataFrame, variáveis e transformações.
382
 
383
  Retorna:
384
- tuple: (df, coluna_y, colunas_x, transformacao_y, transformacoes_x, dicotomicas, codigo_alocado, percentuais, mensagem, sucesso, elaborador, outliers_excluidos, periodo_dados_mercado, config_avaliacao)
385
  """
386
  try:
387
  pacote = load(caminho)
@@ -458,6 +464,7 @@ def carregar_dai(caminho):
458
  df=df,
459
  )
460
  elaborador = pacote.get("elaborador", None)
 
461
  msg = f"Modelo .dai carregado: {os.path.basename(caminho)} ({len(df)} dados, Y={coluna_y}, {len(colunas_x)} variáveis X)"
462
  return (
463
  df,
@@ -471,6 +478,7 @@ def carregar_dai(caminho):
471
  msg,
472
  True,
473
  elaborador,
 
474
  outliers_excluidos,
475
  periodo_dados_mercado,
476
  config_avaliacao,
@@ -489,6 +497,7 @@ def carregar_dai(caminho):
489
  f"Erro ao carregar .dai: {str(e)}",
490
  False,
491
  None,
 
492
  [],
493
  {"data_inicial": None, "data_final": None},
494
  {"tipo_y": None, "coluna_area": None},
@@ -2026,7 +2035,8 @@ def exportar_base_csv(df):
2026
 
2027
  def exportar_modelo_dai(resultado_modelo, df_original, df_completo=None, estatisticas=None,
2028
  nome_arquivo="", elaborador=None, outliers_excluidos=None,
2029
- periodo_dados_mercado=None, tipo_y=None, coluna_area=None):
 
2030
  """
2031
  Exporta o modelo em formato .dai v2 (estrutura nested).
2032
 
@@ -2041,6 +2051,7 @@ def exportar_modelo_dai(resultado_modelo, df_original, df_completo=None, estatis
2041
  periodo_dados_mercado: dict com data_inicial e data_final dos dados de mercado
2042
  tipo_y: natureza do Y ("unitario" ou "total")
2043
  coluna_area: nome da coluna de área usada nas conversões
 
2044
 
2045
  Retorna:
2046
  tuple: (caminho_arquivo, mensagem)
@@ -2160,6 +2171,7 @@ def exportar_modelo_dai(resultado_modelo, df_original, df_completo=None, estatis
2160
  pacote = {
2161
  "versao": 2,
2162
  "elaborador": elaborador,
 
2163
  "periodo_dados_mercado": _normalizar_periodo_dados_mercado(periodo_dados_mercado),
2164
  "avaliacao": normalizar_config_avaliacao_modelo(
2165
  {"tipo_y": tipo_y, "coluna_area": coluna_area},
 
376
  }
377
 
378
 
379
+ def normalizar_observacao_modelo(observacao):
380
+ """Normaliza a observação livre salva junto do modelo."""
381
+ texto = str(observacao or "").strip()
382
+ return texto or None
383
+
384
+
385
  def carregar_dai(caminho):
386
  """
387
  Carrega arquivo .dai e extrai DataFrame, variáveis e transformações.
388
 
389
  Retorna:
390
+ tuple: (df, coluna_y, colunas_x, transformacao_y, transformacoes_x, dicotomicas, codigo_alocado, percentuais, mensagem, sucesso, elaborador, observacao_modelo, outliers_excluidos, periodo_dados_mercado, config_avaliacao)
391
  """
392
  try:
393
  pacote = load(caminho)
 
464
  df=df,
465
  )
466
  elaborador = pacote.get("elaborador", None)
467
+ observacao_modelo = normalizar_observacao_modelo(pacote.get("observacao_modelo"))
468
  msg = f"Modelo .dai carregado: {os.path.basename(caminho)} ({len(df)} dados, Y={coluna_y}, {len(colunas_x)} variáveis X)"
469
  return (
470
  df,
 
478
  msg,
479
  True,
480
  elaborador,
481
+ observacao_modelo,
482
  outliers_excluidos,
483
  periodo_dados_mercado,
484
  config_avaliacao,
 
497
  f"Erro ao carregar .dai: {str(e)}",
498
  False,
499
  None,
500
+ None,
501
  [],
502
  {"data_inicial": None, "data_final": None},
503
  {"tipo_y": None, "coluna_area": None},
 
2035
 
2036
  def exportar_modelo_dai(resultado_modelo, df_original, df_completo=None, estatisticas=None,
2037
  nome_arquivo="", elaborador=None, outliers_excluidos=None,
2038
+ periodo_dados_mercado=None, tipo_y=None, coluna_area=None,
2039
+ observacao_modelo=None):
2040
  """
2041
  Exporta o modelo em formato .dai v2 (estrutura nested).
2042
 
 
2051
  periodo_dados_mercado: dict com data_inicial e data_final dos dados de mercado
2052
  tipo_y: natureza do Y ("unitario" ou "total")
2053
  coluna_area: nome da coluna de área usada nas conversões
2054
+ observacao_modelo: texto livre associado ao modelo
2055
 
2056
  Retorna:
2057
  tuple: (caminho_arquivo, mensagem)
 
2171
  pacote = {
2172
  "versao": 2,
2173
  "elaborador": elaborador,
2174
+ "observacao_modelo": normalizar_observacao_modelo(observacao_modelo),
2175
  "periodo_dados_mercado": _normalizar_periodo_dados_mercado(periodo_dados_mercado),
2176
  "avaliacao": normalizar_config_avaliacao_modelo(
2177
  {"tipo_y": tipo_y, "coluna_area": coluna_area},
backend/app/core/visualizacao/app.py CHANGED
@@ -1460,6 +1460,7 @@ def _formatar_badge_completo(pacote, nome_modelo=""):
1460
  return texto
1461
 
1462
  model_name = str(nome_modelo or "").strip() or "-"
 
1463
 
1464
  periodo = pacote.get("periodo_dados_mercado") or {}
1465
  data_inicial = _data_br(periodo.get("data_inicial"))
@@ -1555,6 +1556,13 @@ def _formatar_badge_completo(pacote, nome_modelo=""):
1555
  + (f"<div class='elaborador-badge-meta'>{_esc(meta_txt)}</div>" if meta_txt else "")
1556
  )
1557
  )
 
 
 
 
 
 
 
1558
 
1559
  return (
1560
  "<div class='subpanel section1-group'>"
@@ -1562,10 +1570,7 @@ def _formatar_badge_completo(pacote, nome_modelo=""):
1562
  "<div class='modelo-info-card'>"
1563
  "<div class='modelo-info-split'>"
1564
  "<div class='modelo-info-col'>"
1565
- "<div class='modelo-info-stack-block'>"
1566
- "<div class='elaborador-badge-title'>NOME DO MODELO:</div>"
1567
- f"<div class='elaborador-badge-name'>{_esc(model_name)}</div>"
1568
- "</div>"
1569
  "<div class='modelo-info-stack-block'>"
1570
  "<div class='elaborador-badge-title'>ELABORADO POR:</div>"
1571
  + elaborador_html +
 
1460
  return texto
1461
 
1462
  model_name = str(nome_modelo or "").strip() or "-"
1463
+ observacao_modelo = str(pacote.get("observacao_modelo") or "").strip()
1464
 
1465
  periodo = pacote.get("periodo_dados_mercado") or {}
1466
  data_inicial = _data_br(periodo.get("data_inicial"))
 
1556
  + (f"<div class='elaborador-badge-meta'>{_esc(meta_txt)}</div>" if meta_txt else "")
1557
  )
1558
  )
1559
+ nome_modelo_html = (
1560
+ "<div class='modelo-info-stack-block'>"
1561
+ "<div class='elaborador-badge-title'>NOME DO MODELO:</div>"
1562
+ f"<div class='elaborador-badge-name'>{_esc(model_name)}</div>"
1563
+ + (f"<div class='elaborador-badge-meta'>{_esc(observacao_modelo)}</div>" if observacao_modelo else "")
1564
+ + "</div>"
1565
+ )
1566
 
1567
  return (
1568
  "<div class='subpanel section1-group'>"
 
1570
  "<div class='modelo-info-card'>"
1571
  "<div class='modelo-info-split'>"
1572
  "<div class='modelo-info-col'>"
1573
+ + nome_modelo_html +
 
 
 
1574
  "<div class='modelo-info-stack-block'>"
1575
  "<div class='elaborador-badge-title'>ELABORADO POR:</div>"
1576
  + elaborador_html +
backend/app/models/session.py CHANGED
@@ -56,6 +56,7 @@ class SessionState:
56
  graficos_dispersao_cache: dict[str, dict[str, Any]] = field(default_factory=dict)
57
 
58
  elaborador: dict[str, Any] | None = None
 
59
 
60
  def reset_modelo(self) -> None:
61
  self.resultados_busca = []
@@ -66,6 +67,7 @@ class SessionState:
66
  self.transformacao_y = "(x)"
67
  self.transformacoes_x = {}
68
  self.graficos_dispersao_cache = {}
 
69
 
70
  def reset_visualizacao(self) -> None:
71
  self.pacote_visualizacao = None
 
56
  graficos_dispersao_cache: dict[str, dict[str, Any]] = field(default_factory=dict)
57
 
58
  elaborador: dict[str, Any] | None = None
59
+ observacao_modelo: str | None = None
60
 
61
  def reset_modelo(self) -> None:
62
  self.resultados_busca = []
 
67
  self.transformacao_y = "(x)"
68
  self.transformacoes_x = {}
69
  self.graficos_dispersao_cache = {}
70
+ self.observacao_modelo = None
71
 
72
  def reset_visualizacao(self) -> None:
73
  self.pacote_visualizacao = None
backend/app/services/elaboracao_service.py CHANGED
@@ -39,6 +39,7 @@ from app.core.elaboracao.core import (
39
  exportar_base_csv,
40
  exportar_modelo_dai,
41
  identificar_coluna_y_padrao,
 
42
  obter_colunas_numericas,
43
  avaliar_imovel,
44
  testar_micronumerosidade,
@@ -1011,6 +1012,7 @@ def load_dai_for_elaboracao(session: SessionState, caminho_arquivo: str) -> dict
1011
  msg,
1012
  sucesso,
1013
  elaborador,
 
1014
  outliers_excluidos,
1015
  periodo_dados_mercado,
1016
  config_avaliacao,
@@ -1020,10 +1022,12 @@ def load_dai_for_elaboracao(session: SessionState, caminho_arquivo: str) -> dict
1020
  raise HTTPException(status_code=400, detail=msg)
1021
 
1022
  session.elaborador = elaborador
 
1023
  outliers_carregados = _clean_int_list(outliers_excluidos)
1024
  periodo_normalizado = _normalizar_periodo_dados_mercado(periodo_dados_mercado)
1025
 
1026
  base = _set_dataframe_base(session, df, clear_models=True)
 
1027
  session.coluna_data_mercado = periodo_normalizado["coluna_data"]
1028
  session.periodo_dados_mercado_inicio = periodo_normalizado["data_inicial"]
1029
  session.periodo_dados_mercado_fim = periodo_normalizado["data_final"]
@@ -1069,6 +1073,7 @@ def load_dai_for_elaboracao(session: SessionState, caminho_arquivo: str) -> dict
1069
  "outliers_html": html_outliers,
1070
  "contexto": _selection_context(session),
1071
  "elaborador": sanitize_value(elaborador),
 
1072
  **_payload_data_mercado(session),
1073
  }
1074
 
@@ -2373,7 +2378,12 @@ def aplicar_coluna_data_mercado(session: SessionState, coluna_data: str) -> dict
2373
  }
2374
 
2375
 
2376
- def exportar_modelo(session: SessionState, nome_arquivo: str, elaborador: dict[str, Any] | None = None) -> tuple[str, str]:
 
 
 
 
 
2377
  if session.resultado_modelo is None:
2378
  raise HTTPException(status_code=400, detail="Ajuste um modelo antes de exportar")
2379
  if session.df_filtrado is None or session.df_original is None:
@@ -2382,6 +2392,10 @@ def exportar_modelo(session: SessionState, nome_arquivo: str, elaborador: dict[s
2382
  if not nome_arquivo or not nome_arquivo.strip():
2383
  raise HTTPException(status_code=400, detail="Informe o nome do arquivo")
2384
 
 
 
 
 
2385
  caminho, msg = exportar_modelo_dai(
2386
  session.resultado_modelo,
2387
  session.df_filtrado,
@@ -2397,6 +2411,7 @@ def exportar_modelo(session: SessionState, nome_arquivo: str, elaborador: dict[s
2397
  },
2398
  tipo_y=session.tipo_y,
2399
  coluna_area=session.coluna_area,
 
2400
  )
2401
 
2402
  if not caminho:
@@ -2405,6 +2420,46 @@ def exportar_modelo(session: SessionState, nome_arquivo: str, elaborador: dict[s
2405
  return caminho, msg
2406
 
2407
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2408
  def exportar_base(session: SessionState, usar_filtrado: bool = True) -> str:
2409
  df = session.df_filtrado if usar_filtrado else session.df_original
2410
  caminho = exportar_base_csv(df)
 
39
  exportar_base_csv,
40
  exportar_modelo_dai,
41
  identificar_coluna_y_padrao,
42
+ normalizar_observacao_modelo,
43
  obter_colunas_numericas,
44
  avaliar_imovel,
45
  testar_micronumerosidade,
 
1012
  msg,
1013
  sucesso,
1014
  elaborador,
1015
+ observacao_modelo,
1016
  outliers_excluidos,
1017
  periodo_dados_mercado,
1018
  config_avaliacao,
 
1022
  raise HTTPException(status_code=400, detail=msg)
1023
 
1024
  session.elaborador = elaborador
1025
+ observacao_modelo_normalizada = normalizar_observacao_modelo(observacao_modelo)
1026
  outliers_carregados = _clean_int_list(outliers_excluidos)
1027
  periodo_normalizado = _normalizar_periodo_dados_mercado(periodo_dados_mercado)
1028
 
1029
  base = _set_dataframe_base(session, df, clear_models=True)
1030
+ session.observacao_modelo = observacao_modelo_normalizada
1031
  session.coluna_data_mercado = periodo_normalizado["coluna_data"]
1032
  session.periodo_dados_mercado_inicio = periodo_normalizado["data_inicial"]
1033
  session.periodo_dados_mercado_fim = periodo_normalizado["data_final"]
 
1073
  "outliers_html": html_outliers,
1074
  "contexto": _selection_context(session),
1075
  "elaborador": sanitize_value(elaborador),
1076
+ "observacao_modelo": sanitize_value(observacao_modelo_normalizada),
1077
  **_payload_data_mercado(session),
1078
  }
1079
 
 
2378
  }
2379
 
2380
 
2381
+ def exportar_modelo(
2382
+ session: SessionState,
2383
+ nome_arquivo: str,
2384
+ elaborador: dict[str, Any] | None = None,
2385
+ observacao_modelo: str | None = None,
2386
+ ) -> tuple[str, str]:
2387
  if session.resultado_modelo is None:
2388
  raise HTTPException(status_code=400, detail="Ajuste um modelo antes de exportar")
2389
  if session.df_filtrado is None or session.df_original is None:
 
2392
  if not nome_arquivo or not nome_arquivo.strip():
2393
  raise HTTPException(status_code=400, detail="Informe o nome do arquivo")
2394
 
2395
+ session.observacao_modelo = normalizar_observacao_modelo(
2396
+ observacao_modelo if observacao_modelo is not None else session.observacao_modelo
2397
+ )
2398
+
2399
  caminho, msg = exportar_modelo_dai(
2400
  session.resultado_modelo,
2401
  session.df_filtrado,
 
2411
  },
2412
  tipo_y=session.tipo_y,
2413
  coluna_area=session.coluna_area,
2414
+ observacao_modelo=session.observacao_modelo,
2415
  )
2416
 
2417
  if not caminho:
 
2420
  return caminho, msg
2421
 
2422
 
2423
+ def salvar_modelo_repositorio(
2424
+ session: SessionState,
2425
+ nome_arquivo: str,
2426
+ elaborador: dict[str, Any] | None = None,
2427
+ observacao_modelo: str | None = None,
2428
+ actor: str | None = None,
2429
+ confirmar_substituicao: bool = False,
2430
+ ) -> dict[str, Any]:
2431
+ resolved = model_repository.resolve_model_repository()
2432
+ if resolved.provider != "hf_dataset":
2433
+ raise HTTPException(
2434
+ status_code=400,
2435
+ detail="Salvar direto no repositorio so esta disponivel quando o repositorio usa HF Dataset.",
2436
+ )
2437
+
2438
+ caminho, _ = exportar_modelo(
2439
+ session,
2440
+ nome_arquivo,
2441
+ elaborador=elaborador,
2442
+ observacao_modelo=observacao_modelo,
2443
+ )
2444
+ arquivo = Path(caminho)
2445
+ if not arquivo.exists():
2446
+ raise HTTPException(status_code=500, detail="Falha ao gerar o arquivo .dai para envio ao repositorio.")
2447
+
2448
+ resultado = model_repository.upsert_repository_models(
2449
+ [(arquivo.name, arquivo.read_bytes())],
2450
+ actor=actor,
2451
+ confirmar_substituicao=confirmar_substituicao,
2452
+ )
2453
+ contexto = listar_modelos_repositorio()
2454
+ return sanitize_value(
2455
+ {
2456
+ "status": "Modelo salvo no repositorio.",
2457
+ "resultado_upload": resultado,
2458
+ **contexto,
2459
+ }
2460
+ )
2461
+
2462
+
2463
  def exportar_base(session: SessionState, usar_filtrado: bool = True) -> str:
2464
  df = session.df_filtrado if usar_filtrado else session.df_original
2465
  caminho = exportar_base_csv(df)
backend/app/services/visualizacao_service.py CHANGED
@@ -20,6 +20,7 @@ from app.core.elaboracao.core import (
20
  exportar_avaliacoes_excel,
21
  normalizar_coluna_area,
22
  normalizar_config_avaliacao_modelo,
 
23
  )
24
  from app.core.elaboracao.formatadores import formatar_avaliacao_html
25
  from app.models.session import SessionState
@@ -253,6 +254,7 @@ def carregar_modelo(session: SessionState, caminho_arquivo: str) -> dict[str, An
253
  "status": "",
254
  "badge_html": badge_html,
255
  "nome_modelo": nome_modelo,
 
256
  }
257
 
258
 
@@ -297,6 +299,7 @@ def _extrair_modelo_info(pacote: dict[str, Any]) -> dict[str, Any]:
297
  "nome_y": nome_y.strip(),
298
  "tipo_y": config_avaliacao.get("tipo_y"),
299
  "coluna_area": config_avaliacao.get("coluna_area"),
 
300
  "transformacao_y": transformacao_y,
301
  "colunas_x": colunas_x,
302
  "transformacoes_x": transformacoes_x,
 
20
  exportar_avaliacoes_excel,
21
  normalizar_coluna_area,
22
  normalizar_config_avaliacao_modelo,
23
+ normalizar_observacao_modelo,
24
  )
25
  from app.core.elaboracao.formatadores import formatar_avaliacao_html
26
  from app.models.session import SessionState
 
254
  "status": "",
255
  "badge_html": badge_html,
256
  "nome_modelo": nome_modelo,
257
+ "observacao_modelo": normalizar_observacao_modelo(pacote.get("observacao_modelo")),
258
  }
259
 
260
 
 
299
  "nome_y": nome_y.strip(),
300
  "tipo_y": config_avaliacao.get("tipo_y"),
301
  "coluna_area": config_avaliacao.get("coluna_area"),
302
+ "observacao_modelo": normalizar_observacao_modelo(pacote.get("observacao_modelo")),
303
  "transformacao_y": transformacao_y,
304
  "colunas_x": colunas_x,
305
  "transformacoes_x": transformacoes_x,
frontend/src/App.jsx CHANGED
@@ -587,7 +587,7 @@ export default function App() {
587
  </div>
588
 
589
  <div className="tab-pane" hidden={activeTab !== 'Elaboração/Edição'}>
590
- <ElaboracaoTab sessionId={sessionId} />
591
  </div>
592
 
593
  <div className="tab-pane" hidden={activeTab !== 'Repositório de Modelos'}>
 
587
  </div>
588
 
589
  <div className="tab-pane" hidden={activeTab !== 'Elaboração/Edição'}>
590
+ <ElaboracaoTab sessionId={sessionId} authUser={authUser} />
591
  </div>
592
 
593
  <div className="tab-pane" hidden={activeTab !== 'Repositório de Modelos'}>
frontend/src/api.js CHANGED
@@ -270,16 +270,28 @@ export const api = {
270
  }),
271
  exportEvaluationElab: async (sessionId) => getBlob(`/api/elaboracao/evaluation/export?session_id=${encodeURIComponent(sessionId)}`),
272
  exportEquationElab: async (sessionId, mode = 'excel') => getBlob(`/api/elaboracao/equation/export?session_id=${encodeURIComponent(sessionId)}&mode=${encodeURIComponent(String(mode || 'excel'))}`),
273
- exportModel: async (sessionId, nomeArquivo, elaborador) => {
274
  const path = '/api/elaboracao/export-model'
275
  const response = await fetchWithDiagnostics(path, {
276
  method: 'POST',
277
  headers: authHeaders({ 'Content-Type': 'application/json' }),
278
- body: JSON.stringify({ session_id: sessionId, nome_arquivo: nomeArquivo, elaborador }),
 
 
 
 
 
279
  })
280
  await handleResponse(response, path)
281
  return responseBlobWithDiagnostics(response, path)
282
  },
 
 
 
 
 
 
 
283
  exportBase: (sessionId, filtered = true) => getBlob(`/api/elaboracao/export-base?session_id=${encodeURIComponent(sessionId)}&filtered=${String(filtered)}`),
284
  updateElaboracaoMap: (sessionId, variavelMapa, modoMapa = 'pontos') => postJson('/api/elaboracao/map/update', {
285
  session_id: sessionId,
 
270
  }),
271
  exportEvaluationElab: async (sessionId) => getBlob(`/api/elaboracao/evaluation/export?session_id=${encodeURIComponent(sessionId)}`),
272
  exportEquationElab: async (sessionId, mode = 'excel') => getBlob(`/api/elaboracao/equation/export?session_id=${encodeURIComponent(sessionId)}&mode=${encodeURIComponent(String(mode || 'excel'))}`),
273
+ exportModel: async (sessionId, nomeArquivo, elaborador, observacaoModelo = '') => {
274
  const path = '/api/elaboracao/export-model'
275
  const response = await fetchWithDiagnostics(path, {
276
  method: 'POST',
277
  headers: authHeaders({ 'Content-Type': 'application/json' }),
278
+ body: JSON.stringify({
279
+ session_id: sessionId,
280
+ nome_arquivo: nomeArquivo,
281
+ elaborador,
282
+ observacao_modelo: observacaoModelo,
283
+ }),
284
  })
285
  await handleResponse(response, path)
286
  return responseBlobWithDiagnostics(response, path)
287
  },
288
+ saveModelRepository: (sessionId, nomeArquivo, elaborador, observacaoModelo = '', confirmarSubstituicao = false) => postJson('/api/elaboracao/save-model-repository', {
289
+ session_id: sessionId,
290
+ nome_arquivo: nomeArquivo,
291
+ elaborador,
292
+ observacao_modelo: observacaoModelo,
293
+ confirmar_substituicao: confirmarSubstituicao,
294
+ }),
295
  exportBase: (sessionId, filtered = true) => getBlob(`/api/elaboracao/export-base?session_id=${encodeURIComponent(sessionId)}&filtered=${String(filtered)}`),
296
  updateElaboracaoMap: (sessionId, variavelMapa, modoMapa = 'pontos') => postJson('/api/elaboracao/map/update', {
297
  session_id: sessionId,
frontend/src/components/AvaliacaoTab.jsx CHANGED
@@ -444,6 +444,19 @@ function formatarFonteRepositorio(fonte) {
444
  return 'Fonte: pasta local'
445
  }
446
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  const BASE_COMPARACAO_SEM_BASE = '__none__'
448
 
449
  export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
@@ -451,6 +464,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
451
  const [error, setError] = useState('')
452
  const [status, setStatus] = useState('')
453
  const [badgeHtml, setBadgeHtml] = useState('')
 
454
 
455
  const [uploadedFile, setUploadedFile] = useState(null)
456
  const [uploadDragOver, setUploadDragOver] = useState(false)
@@ -688,6 +702,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
688
  resetCamposAvaliacao(campos)
689
  setEquacaoSab(String(resp?.equacoes?.excel_sab || ''))
690
  setModeloAtualNome(String(nomeModelo || '').trim())
 
691
  }
692
 
693
  async function withBusy(fn) {
@@ -730,7 +745,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
730
  await withBusy(async () => {
731
  const uploadResp = await api.uploadVisualizacaoFile(sessionId, arquivoUpload)
732
  setStatus(uploadResp?.status || '')
733
- setBadgeHtml(uploadResp?.badge_html || '')
734
  const nomeModelo = uploadResp?.nome_modelo || arquivoUpload.name || ''
735
  const contextoResp = await api.evaluationContextViz(sessionId)
736
  aplicarRespostaExibicao(contextoResp, nomeModelo)
@@ -753,7 +768,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
753
  const tentativas = [alvoModeloId, ...(fallbackChaves || [])]
754
  const { uploadResp, modeloIdUsado } = await carregarModeloRepositorioComFallback(tentativas)
755
  setStatus(uploadResp?.status || '')
756
- setBadgeHtml(uploadResp?.badge_html || '')
757
  setRepoModeloSelecionado(modeloIdUsado)
758
  const modeloSelecionado = repoModeloOptions.find((item) => item.value === modeloIdUsado)
759
  const nomeModelo = uploadResp?.nome_modelo || modeloSelecionado?.label || ''
@@ -1110,6 +1125,12 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
1110
  )}
1111
  {status ? <div className="status-line">{status}</div> : null}
1112
  {badgeHtml ? <div className="upload-badge-block" dangerouslySetInnerHTML={{ __html: badgeHtml }} /> : null}
 
 
 
 
 
 
1113
  </div>
1114
 
1115
  {!modeloPronto ? (
 
444
  return 'Fonte: pasta local'
445
  }
446
 
447
+ function removerObservacaoDoBadgeHtml(html) {
448
+ const markup = String(html || '').trim()
449
+ if (!markup || typeof document === 'undefined') return markup
450
+
451
+ const root = document.createElement('div')
452
+ root.innerHTML = markup
453
+ const observacao = root.querySelector('.modelo-info-stack-block:first-child .elaborador-badge-meta')
454
+ if (observacao) {
455
+ observacao.remove()
456
+ }
457
+ return root.innerHTML
458
+ }
459
+
460
  const BASE_COMPARACAO_SEM_BASE = '__none__'
461
 
462
  export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
 
464
  const [error, setError] = useState('')
465
  const [status, setStatus] = useState('')
466
  const [badgeHtml, setBadgeHtml] = useState('')
467
+ const [modeloObservacao, setModeloObservacao] = useState('')
468
 
469
  const [uploadedFile, setUploadedFile] = useState(null)
470
  const [uploadDragOver, setUploadDragOver] = useState(false)
 
702
  resetCamposAvaliacao(campos)
703
  setEquacaoSab(String(resp?.equacoes?.excel_sab || ''))
704
  setModeloAtualNome(String(nomeModelo || '').trim())
705
+ setModeloObservacao(String(resp?.meta_modelo?.observacao_modelo || '').trim())
706
  }
707
 
708
  async function withBusy(fn) {
 
745
  await withBusy(async () => {
746
  const uploadResp = await api.uploadVisualizacaoFile(sessionId, arquivoUpload)
747
  setStatus(uploadResp?.status || '')
748
+ setBadgeHtml(removerObservacaoDoBadgeHtml(uploadResp?.badge_html || ''))
749
  const nomeModelo = uploadResp?.nome_modelo || arquivoUpload.name || ''
750
  const contextoResp = await api.evaluationContextViz(sessionId)
751
  aplicarRespostaExibicao(contextoResp, nomeModelo)
 
768
  const tentativas = [alvoModeloId, ...(fallbackChaves || [])]
769
  const { uploadResp, modeloIdUsado } = await carregarModeloRepositorioComFallback(tentativas)
770
  setStatus(uploadResp?.status || '')
771
+ setBadgeHtml(removerObservacaoDoBadgeHtml(uploadResp?.badge_html || ''))
772
  setRepoModeloSelecionado(modeloIdUsado)
773
  const modeloSelecionado = repoModeloOptions.find((item) => item.value === modeloIdUsado)
774
  const nomeModelo = uploadResp?.nome_modelo || modeloSelecionado?.label || ''
 
1125
  )}
1126
  {status ? <div className="status-line">{status}</div> : null}
1127
  {badgeHtml ? <div className="upload-badge-block" dangerouslySetInnerHTML={{ __html: badgeHtml }} /> : null}
1128
+ {modeloObservacao ? (
1129
+ <div className="elaborador-badge modelo-observacao-badge">
1130
+ <div className="elaborador-badge-title">OBSERVAÇÃO</div>
1131
+ <div className="modelo-observacao-text">{modeloObservacao}</div>
1132
+ </div>
1133
+ ) : null}
1134
  </div>
1135
 
1136
  {!modeloPronto ? (
frontend/src/components/ElaboracaoTab.jsx CHANGED
@@ -745,6 +745,7 @@ function buildLoadedModelInfo(resp) {
745
 
746
  return {
747
  nome_modelo: nomeModelo,
 
748
  coluna_y: colunaY,
749
  tipo_y: normalizarTipoY(contexto.tipo_y),
750
  coluna_area: String(contexto.coluna_area || '').trim(),
@@ -859,7 +860,7 @@ function DiagnosticPngCard({ title, pngPayload, alt }) {
859
  )
860
  }
861
 
862
- export default function ElaboracaoTab({ sessionId }) {
863
  const [loading, setLoading] = useState(false)
864
  const [downloadingAssets, setDownloadingAssets] = useState(false)
865
  const [error, setError] = useState('')
@@ -877,6 +878,7 @@ export default function ElaboracaoTab({ sessionId }) {
877
  const [repoModelos, setRepoModelos] = useState([])
878
  const [repoModeloSelecionado, setRepoModeloSelecionado] = useState('')
879
  const [repoModelosLoading, setRepoModelosLoading] = useState(false)
 
880
  const [repoFonteModelos, setRepoFonteModelos] = useState('')
881
  const [repoModeloDropdownOpen, setRepoModeloDropdownOpen] = useState(false)
882
 
@@ -984,8 +986,10 @@ export default function ElaboracaoTab({ sessionId }) {
984
  const [knnDetalheInfo, setKnnDetalheInfo] = useState(null)
985
 
986
  const [nomeArquivoExport, setNomeArquivoExport] = useState('')
 
987
  const [avaliadores, setAvaliadores] = useState([])
988
  const [avaliadorSelecionado, setAvaliadorSelecionado] = useState('')
 
989
  const [tipoFonteDados, setTipoFonteDados] = useState('')
990
  const [selectionAppliedSnapshot, setSelectionAppliedSnapshot] = useState(() => buildSelectionSnapshot())
991
  const [buscaTransformAppliedSnapshot, setBuscaTransformAppliedSnapshot] = useState(() => buildGrauSnapshot(0, 0))
@@ -1027,6 +1031,14 @@ export default function ElaboracaoTab({ sessionId }) {
1027
  [colunasX, colunasXDisponiveis],
1028
  )
1029
  const conselhoRegistro = useMemo(() => formatConselhoRegistro(elaborador), [elaborador])
 
 
 
 
 
 
 
 
1030
  const elaboradorMeta = useMemo(() => {
1031
  if (!elaborador) return []
1032
  return [elaborador.cargo, conselhoRegistro, elaborador.matricula_sem_digito ? `Matricula ${elaborador.matricula_sem_digito}` : '', elaborador.lotacao].filter(Boolean)
@@ -1901,6 +1913,7 @@ export default function ElaboracaoTab({ sessionId }) {
1901
  if (!ativo) return
1902
  setRepoModelos([])
1903
  setRepoModeloSelecionado('')
 
1904
  setRepoFonteModelos('')
1905
  })
1906
  .finally(() => {
@@ -1966,9 +1979,19 @@ export default function ElaboracaoTab({ sessionId }) {
1966
  return 'Fonte: pasta local'
1967
  }
1968
 
 
 
 
 
 
 
 
 
 
1969
  function aplicarRespostaModelosRepositorio(resp) {
1970
  const modelos = Array.isArray(resp?.modelos) ? resp.modelos : []
1971
  setRepoModelos(modelos)
 
1972
  setRepoFonteModelos(formatarFonteRepositorio(resp?.fonte || null))
1973
  setRepoModeloSelecionado((prev) => {
1974
  const atual = String(prev || '')
@@ -1988,6 +2011,7 @@ export default function ElaboracaoTab({ sessionId }) {
1988
  setImportacaoErro(err.message || 'Falha ao carregar modelos do repositório.')
1989
  setRepoModelos([])
1990
  setRepoModeloSelecionado('')
 
1991
  setRepoFonteModelos('')
1992
  } finally {
1993
  setRepoModelosLoading(false)
@@ -2273,6 +2297,7 @@ export default function ElaboracaoTab({ sessionId }) {
2273
  setGeoFalhasHtml('')
2274
  setGeoCorrecoes([])
2275
  setElaborador(resp.elaborador || null)
 
2276
  setModeloCarregadoInfo(buildLoadedModelInfo(resp))
2277
  setAvaliadorSelecionado(resp.elaborador?.nome_completo || '')
2278
  const fileName = String(meta.fileName || uploadedFile?.name || arquivoCarregadoInfo?.nome_arquivo || '').trim()
@@ -2477,6 +2502,7 @@ export default function ElaboracaoTab({ sessionId }) {
2477
  setGeoFalhasHtml('')
2478
  setGeoCorrecoes([])
2479
  setElaborador(resp.elaborador || null)
 
2480
  setModeloCarregadoInfo(null)
2481
  setAvaliadorSelecionado(resp.elaborador?.nome_completo || '')
2482
  setRequiresSheet(false)
@@ -3245,12 +3271,71 @@ export default function ElaboracaoTab({ sessionId }) {
3245
  const nomeExport = String(nomeArquivoExport || '').trim()
3246
  if (!sessionId || !nomeExport) return
3247
  await withBusy(async () => {
 
3248
  const avaliadorEscolhido = avaliadores.find((item) => item.nome_completo === avaliadorSelecionado) || null
3249
- const blob = await api.exportModel(sessionId, nomeExport, avaliadorEscolhido || elaborador || null)
 
 
 
 
 
3250
  downloadBlob(blob, `${nomeExport.replace(/\.dai$/i, '')}.dai`)
3251
  })
3252
  }
3253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3254
  async function onExportBase() {
3255
  if (!sessionId) return
3256
  await withBusy(async () => {
@@ -3903,6 +3988,12 @@ export default function ElaboracaoTab({ sessionId }) {
3903
  </div>
3904
  </div>
3905
  </div>
 
 
 
 
 
 
3906
  </>
3907
  ) : null}
3908
  </div>
@@ -5830,26 +5921,94 @@ export default function ElaboracaoTab({ sessionId }) {
5830
  </SectionBlock>
5831
 
5832
  <SectionBlock step="19" title="Exportar Modelo" subtitle="Geração do pacote .dai e download da base tratada.">
5833
- <div className="row">
5834
- <label>Nome do arquivo (.dai)</label>
5835
- <input
5836
- type="text"
5837
- value={nomeArquivoExport}
5838
- onChange={(e) => setNomeArquivoExport(e.target.value)}
5839
- placeholder="Digite o nome do arquivo"
5840
- />
5841
- <label>Avaliador</label>
5842
- <select value={avaliadorSelecionado} onChange={(e) => setAvaliadorSelecionado(e.target.value)}>
5843
- <option value="">Manter elaborador do modelo (se houver)</option>
5844
- {avaliadores.map((item) => (
5845
- <option key={`aval-exp-${item.nome_completo}`} value={item.nome_completo}>
5846
- {item.nome_completo}
5847
- </option>
5848
- ))}
5849
- </select>
5850
- <button onClick={onExportModel} disabled={loading || !String(nomeArquivoExport || '').trim()}>Exportar modelo</button>
5851
- <button onClick={onExportBase} disabled={loading}>Exportar base CSV</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5852
  </div>
 
5853
  </SectionBlock>
5854
  </>
5855
  ) : null}
 
745
 
746
  return {
747
  nome_modelo: nomeModelo,
748
+ observacao_modelo: String(resp?.observacao_modelo || '').trim(),
749
  coluna_y: colunaY,
750
  tipo_y: normalizarTipoY(contexto.tipo_y),
751
  coluna_area: String(contexto.coluna_area || '').trim(),
 
860
  )
861
  }
862
 
863
+ export default function ElaboracaoTab({ sessionId, authUser }) {
864
  const [loading, setLoading] = useState(false)
865
  const [downloadingAssets, setDownloadingAssets] = useState(false)
866
  const [error, setError] = useState('')
 
878
  const [repoModelos, setRepoModelos] = useState([])
879
  const [repoModeloSelecionado, setRepoModeloSelecionado] = useState('')
880
  const [repoModelosLoading, setRepoModelosLoading] = useState(false)
881
+ const [repoFonteInfo, setRepoFonteInfo] = useState(null)
882
  const [repoFonteModelos, setRepoFonteModelos] = useState('')
883
  const [repoModeloDropdownOpen, setRepoModeloDropdownOpen] = useState(false)
884
 
 
986
  const [knnDetalheInfo, setKnnDetalheInfo] = useState(null)
987
 
988
  const [nomeArquivoExport, setNomeArquivoExport] = useState('')
989
+ const [observacaoModelo, setObservacaoModelo] = useState('')
990
  const [avaliadores, setAvaliadores] = useState([])
991
  const [avaliadorSelecionado, setAvaliadorSelecionado] = useState('')
992
+ const [repositorioSaveStatus, setRepositorioSaveStatus] = useState('')
993
  const [tipoFonteDados, setTipoFonteDados] = useState('')
994
  const [selectionAppliedSnapshot, setSelectionAppliedSnapshot] = useState(() => buildSelectionSnapshot())
995
  const [buscaTransformAppliedSnapshot, setBuscaTransformAppliedSnapshot] = useState(() => buildGrauSnapshot(0, 0))
 
1031
  [colunasX, colunasXDisponiveis],
1032
  )
1033
  const conselhoRegistro = useMemo(() => formatConselhoRegistro(elaborador), [elaborador])
1034
+ const repositorioUploadAdminHabilitado = useMemo(
1035
+ () => String(authUser?.perfil || '').toLowerCase() === 'admin',
1036
+ [authUser],
1037
+ )
1038
+ const repositorioSalvamentoDiretoHabilitado = useMemo(
1039
+ () => repositorioUploadAdminHabilitado && String(repoFonteInfo?.provider || '').toLowerCase() === 'hf_dataset',
1040
+ [repoFonteInfo, repositorioUploadAdminHabilitado],
1041
+ )
1042
  const elaboradorMeta = useMemo(() => {
1043
  if (!elaborador) return []
1044
  return [elaborador.cargo, conselhoRegistro, elaborador.matricula_sem_digito ? `Matricula ${elaborador.matricula_sem_digito}` : '', elaborador.lotacao].filter(Boolean)
 
1913
  if (!ativo) return
1914
  setRepoModelos([])
1915
  setRepoModeloSelecionado('')
1916
+ setRepoFonteInfo(null)
1917
  setRepoFonteModelos('')
1918
  })
1919
  .finally(() => {
 
1979
  return 'Fonte: pasta local'
1980
  }
1981
 
1982
+ function parseSubstituidosErro(err) {
1983
+ if (!err || typeof err !== 'object') return []
1984
+ const detail = err.detail
1985
+ if (!detail || typeof detail !== 'object') return []
1986
+ const lista = detail.substituidos
1987
+ if (!Array.isArray(lista)) return []
1988
+ return lista.map((item) => String(item || '').trim()).filter(Boolean)
1989
+ }
1990
+
1991
  function aplicarRespostaModelosRepositorio(resp) {
1992
  const modelos = Array.isArray(resp?.modelos) ? resp.modelos : []
1993
  setRepoModelos(modelos)
1994
+ setRepoFonteInfo(resp?.fonte || null)
1995
  setRepoFonteModelos(formatarFonteRepositorio(resp?.fonte || null))
1996
  setRepoModeloSelecionado((prev) => {
1997
  const atual = String(prev || '')
 
2011
  setImportacaoErro(err.message || 'Falha ao carregar modelos do repositório.')
2012
  setRepoModelos([])
2013
  setRepoModeloSelecionado('')
2014
+ setRepoFonteInfo(null)
2015
  setRepoFonteModelos('')
2016
  } finally {
2017
  setRepoModelosLoading(false)
 
2297
  setGeoFalhasHtml('')
2298
  setGeoCorrecoes([])
2299
  setElaborador(resp.elaborador || null)
2300
+ setObservacaoModelo(String(resp?.observacao_modelo || '').trim())
2301
  setModeloCarregadoInfo(buildLoadedModelInfo(resp))
2302
  setAvaliadorSelecionado(resp.elaborador?.nome_completo || '')
2303
  const fileName = String(meta.fileName || uploadedFile?.name || arquivoCarregadoInfo?.nome_arquivo || '').trim()
 
2502
  setGeoFalhasHtml('')
2503
  setGeoCorrecoes([])
2504
  setElaborador(resp.elaborador || null)
2505
+ setObservacaoModelo(String(resp?.observacao_modelo || '').trim())
2506
  setModeloCarregadoInfo(null)
2507
  setAvaliadorSelecionado(resp.elaborador?.nome_completo || '')
2508
  setRequiresSheet(false)
 
3271
  const nomeExport = String(nomeArquivoExport || '').trim()
3272
  if (!sessionId || !nomeExport) return
3273
  await withBusy(async () => {
3274
+ setRepositorioSaveStatus('')
3275
  const avaliadorEscolhido = avaliadores.find((item) => item.nome_completo === avaliadorSelecionado) || null
3276
+ const blob = await api.exportModel(
3277
+ sessionId,
3278
+ nomeExport,
3279
+ avaliadorEscolhido || elaborador || null,
3280
+ String(observacaoModelo || '').trim(),
3281
+ )
3282
  downloadBlob(blob, `${nomeExport.replace(/\.dai$/i, '')}.dai`)
3283
  })
3284
  }
3285
 
3286
+ async function onSaveModelRepository() {
3287
+ const nomeExport = String(nomeArquivoExport || '').trim()
3288
+ if (!sessionId || !nomeExport || !repositorioSalvamentoDiretoHabilitado) return
3289
+
3290
+ const senha = window.prompt('Digite a senha para salvar o modelo diretamente no repositório:')
3291
+ if (senha === null) return
3292
+ if (senha !== '123456') {
3293
+ setRepositorioSaveStatus('')
3294
+ setError('Senha incorreta para salvar no repositório.')
3295
+ return
3296
+ }
3297
+
3298
+ const avaliadorEscolhido = avaliadores.find((item) => item.nome_completo === avaliadorSelecionado) || null
3299
+ const elaboradorPayload = avaliadorEscolhido || elaborador || null
3300
+ const observacaoTexto = String(observacaoModelo || '').trim()
3301
+
3302
+ setLoading(true)
3303
+ setError('')
3304
+ setRepositorioSaveStatus('')
3305
+ try {
3306
+ const salvar = async (confirmarSubstituicao) => {
3307
+ const resp = await api.saveModelRepository(
3308
+ sessionId,
3309
+ nomeExport,
3310
+ elaboradorPayload,
3311
+ observacaoTexto,
3312
+ confirmarSubstituicao,
3313
+ )
3314
+ aplicarRespostaModelosRepositorio(resp)
3315
+ setRepositorioSaveStatus(resp?.status || 'Modelo salvo no repositorio.')
3316
+ }
3317
+
3318
+ try {
3319
+ await salvar(false)
3320
+ } catch (err) {
3321
+ const substituidos = parseSubstituidosErro(err)
3322
+ const erroDuplicado = Number(err?.status) === 409 && substituidos.length > 0
3323
+ if (!erroDuplicado) throw err
3324
+
3325
+ const nomes = substituidos.join(', ')
3326
+ const confirma = window.confirm(
3327
+ `Ja existe modelo com este nome no repositorio (${nomes}). Deseja sobrescrever?`,
3328
+ )
3329
+ if (!confirma) return
3330
+ await salvar(true)
3331
+ }
3332
+ } catch (err) {
3333
+ setError(err?.message || 'Falha ao salvar modelo no repositorio.')
3334
+ } finally {
3335
+ setLoading(false)
3336
+ }
3337
+ }
3338
+
3339
  async function onExportBase() {
3340
  if (!sessionId) return
3341
  await withBusy(async () => {
 
3988
  </div>
3989
  </div>
3990
  </div>
3991
+ {modeloCarregadoInfo.observacao_modelo ? (
3992
+ <div className="elaborador-badge modelo-observacao-badge">
3993
+ <div className="elaborador-badge-title">OBSERVAÇÃO</div>
3994
+ <div className="modelo-observacao-text">{modeloCarregadoInfo.observacao_modelo}</div>
3995
+ </div>
3996
+ ) : null}
3997
  </>
3998
  ) : null}
3999
  </div>
 
5921
  </SectionBlock>
5922
 
5923
  <SectionBlock step="19" title="Exportar Modelo" subtitle="Geração do pacote .dai e download da base tratada.">
5924
+ <div className="export-model-layout">
5925
+ <div className="export-model-note-card">
5926
+ <div className="export-model-note-head">
5927
+ <label htmlFor="elaboracao-observacao-modelo" className="export-model-note-label">Observação do modelo</label>
5928
+ <p className="export-model-note-help">
5929
+ Esse texto será salvo e apresentado quando o modelo for visualizado.
5930
+ </p>
5931
+ </div>
5932
+ <textarea
5933
+ id="elaboracao-observacao-modelo"
5934
+ className="export-model-note-input"
5935
+ value={observacaoModelo}
5936
+ onChange={(e) => setObservacaoModelo(e.target.value)}
5937
+ placeholder="Informações relevantes que podem ser úteis ao usuário do modelo."
5938
+ rows={4}
5939
+ />
5940
+ </div>
5941
+
5942
+ <div className="export-model-actions-card">
5943
+ <div className="export-model-fields">
5944
+ <label className="export-model-field">
5945
+ <span>Nome do arquivo (.dai)</span>
5946
+ <input
5947
+ type="text"
5948
+ value={nomeArquivoExport}
5949
+ onChange={(e) => setNomeArquivoExport(e.target.value)}
5950
+ placeholder="Digite o nome do arquivo"
5951
+ />
5952
+ </label>
5953
+
5954
+ <label className="export-model-field">
5955
+ <span>Avaliador</span>
5956
+ <select value={avaliadorSelecionado} onChange={(e) => setAvaliadorSelecionado(e.target.value)}>
5957
+ <option value="">Manter elaborador do modelo (se houver)</option>
5958
+ {avaliadores.map((item) => (
5959
+ <option key={`aval-exp-${item.nome_completo}`} value={item.nome_completo}>
5960
+ {item.nome_completo}
5961
+ </option>
5962
+ ))}
5963
+ </select>
5964
+ </label>
5965
+ </div>
5966
+
5967
+ <div className="export-model-buttons">
5968
+ <button
5969
+ type="button"
5970
+ className="export-model-btn-primary"
5971
+ onClick={onExportModel}
5972
+ disabled={loading || !String(nomeArquivoExport || '').trim()}
5973
+ >
5974
+ Exportar modelo
5975
+ </button>
5976
+ <button
5977
+ type="button"
5978
+ className="export-model-btn-repo"
5979
+ onClick={onSaveModelRepository}
5980
+ disabled={loading || !String(nomeArquivoExport || '').trim() || !repositorioSalvamentoDiretoHabilitado}
5981
+ title={
5982
+ !repositorioUploadAdminHabilitado
5983
+ ? 'Disponivel apenas para administradores.'
5984
+ : !repositorioSalvamentoDiretoHabilitado
5985
+ ? 'Disponivel apenas quando o repositorio usa HF Dataset.'
5986
+ : ''
5987
+ }
5988
+ >
5989
+ Salvar no repositorio
5990
+ </button>
5991
+ <button
5992
+ type="button"
5993
+ className="export-model-btn-base"
5994
+ onClick={onExportBase}
5995
+ disabled={loading}
5996
+ >
5997
+ Exportar base CSV
5998
+ </button>
5999
+ </div>
6000
+ </div>
6001
+ </div>
6002
+ <div className="section1-empty-hint export-model-hint">
6003
+ {repoModelosLoading
6004
+ ? 'Verificando fonte do repositório...'
6005
+ : !repositorioUploadAdminHabilitado
6006
+ ? 'Salvar no repositório fica disponível apenas para administradores.'
6007
+ : repositorioSalvamentoDiretoHabilitado
6008
+ ? `Salvamento direto habilitado. ${repoFonteModelos || ''}`.trim()
6009
+ : 'Salvar no repositório fica desabilitado na versão local.'}
6010
  </div>
6011
+ {repositorioSaveStatus ? <div className="section1-empty-hint export-model-status">{repositorioSaveStatus}</div> : null}
6012
  </SectionBlock>
6013
  </>
6014
  ) : null}
frontend/src/components/PesquisaTab.jsx CHANGED
@@ -679,6 +679,7 @@ export default function PesquisaTab({ sessionId, onUsarModeloEmAvaliacao = null
679
  setModeloAbertoMeta({
680
  id: modelo.id,
681
  nome: modelo.nome_modelo || modelo.arquivo || modelo.id,
 
682
  })
683
  window.requestAnimationFrame(() => {
684
  scrollParaAbasGeraisNoTopo()
@@ -757,7 +758,7 @@ export default function PesquisaTab({ sessionId, onUsarModeloEmAvaliacao = null
757
  <div className="pesquisa-opened-model-head">
758
  <div className="pesquisa-opened-model-title-wrap">
759
  <h3>{modeloAbertoMeta?.nome || 'Modelo'}</h3>
760
- <p>Aceito para o avaliando</p>
761
  </div>
762
  <button
763
  type="button"
 
679
  setModeloAbertoMeta({
680
  id: modelo.id,
681
  nome: modelo.nome_modelo || modelo.arquivo || modelo.id,
682
+ observacao: String(resp?.meta_modelo?.observacao_modelo || '').trim(),
683
  })
684
  window.requestAnimationFrame(() => {
685
  scrollParaAbasGeraisNoTopo()
 
758
  <div className="pesquisa-opened-model-head">
759
  <div className="pesquisa-opened-model-title-wrap">
760
  <h3>{modeloAbertoMeta?.nome || 'Modelo'}</h3>
761
+ {modeloAbertoMeta?.observacao ? <p>{modeloAbertoMeta.observacao}</p> : null}
762
  </div>
763
  <button
764
  type="button"
frontend/src/components/RepositorioTab.jsx CHANGED
@@ -210,6 +210,7 @@ export default function RepositorioTab({ authUser, sessionId }) {
210
  setModeloAbertoMeta({
211
  id: String(item?.id || ''),
212
  nome: item?.nome_modelo || item?.arquivo || String(item?.id || ''),
 
213
  })
214
  } catch (err) {
215
  setModeloAbertoError(err.message || 'Falha ao abrir modelo.')
@@ -256,7 +257,7 @@ export default function RepositorioTab({ authUser, sessionId }) {
256
  <div className="pesquisa-opened-model-head">
257
  <div className="pesquisa-opened-model-title-wrap">
258
  <h3>{modeloAbertoMeta?.nome || 'Modelo'}</h3>
259
- <p>Visualização do modelo do repositório</p>
260
  </div>
261
  <button type="button" className="model-source-back-btn model-source-back-btn-danger" onClick={onVoltarRepositorio} disabled={modeloAbertoLoading}>
262
  Voltar ao repositório
 
210
  setModeloAbertoMeta({
211
  id: String(item?.id || ''),
212
  nome: item?.nome_modelo || item?.arquivo || String(item?.id || ''),
213
+ observacao: String(resp?.meta_modelo?.observacao_modelo || '').trim(),
214
  })
215
  } catch (err) {
216
  setModeloAbertoError(err.message || 'Falha ao abrir modelo.')
 
257
  <div className="pesquisa-opened-model-head">
258
  <div className="pesquisa-opened-model-title-wrap">
259
  <h3>{modeloAbertoMeta?.nome || 'Modelo'}</h3>
260
+ {modeloAbertoMeta?.observacao ? <p>{modeloAbertoMeta.observacao}</p> : null}
261
  </div>
262
  <button type="button" className="model-source-back-btn model-source-back-btn-danger" onClick={onVoltarRepositorio} disabled={modeloAbertoLoading}>
263
  Voltar ao repositório
frontend/src/styles.css CHANGED
@@ -2840,6 +2840,143 @@ button:disabled {
2840
  color: #2a3b4d;
2841
  }
2842
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2843
  .section1-groups {
2844
  display: grid;
2845
  gap: 18px;
@@ -3172,6 +3309,18 @@ button.btn-upload-select {
3172
  font-size: 0.83rem;
3173
  }
3174
 
 
 
 
 
 
 
 
 
 
 
 
 
3175
  .modelo-variaveis-box {
3176
  padding: 9px 11px;
3177
  border-radius: 11px;
 
2840
  color: #2a3b4d;
2841
  }
2842
 
2843
+ .export-model-layout {
2844
+ display: grid;
2845
+ gap: 14px;
2846
+ margin-bottom: 12px;
2847
+ }
2848
+
2849
+ .export-model-note-card,
2850
+ .export-model-actions-card {
2851
+ border: 1px solid #dde7f0;
2852
+ border-radius: 14px;
2853
+ background:
2854
+ linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 251, 255, 0.98) 100%);
2855
+ box-shadow:
2856
+ 0 10px 22px rgba(30, 44, 60, 0.05),
2857
+ inset 0 1px 0 rgba(255, 255, 255, 0.8);
2858
+ padding: 16px;
2859
+ }
2860
+
2861
+ .export-model-note-card {
2862
+ display: grid;
2863
+ gap: 12px;
2864
+ border-left: 4px solid #ffb259;
2865
+ }
2866
+
2867
+ .export-model-note-head {
2868
+ display: grid;
2869
+ gap: 4px;
2870
+ }
2871
+
2872
+ .export-model-note-label {
2873
+ margin: 0;
2874
+ font-family: 'Sora', sans-serif;
2875
+ font-size: 0.94rem;
2876
+ color: #243547;
2877
+ }
2878
+
2879
+ .export-model-note-help {
2880
+ margin: 0;
2881
+ color: #61778d;
2882
+ font-size: 0.84rem;
2883
+ line-height: 1.45;
2884
+ }
2885
+
2886
+ .export-model-note-input {
2887
+ min-height: 112px;
2888
+ resize: vertical;
2889
+ line-height: 1.5;
2890
+ padding: 12px 14px;
2891
+ background:
2892
+ linear-gradient(180deg, #ffffff 0%, #fdfefe 100%);
2893
+ border-color: #ccd9e5;
2894
+ box-shadow: inset 0 1px 2px rgba(25, 39, 53, 0.04);
2895
+ }
2896
+
2897
+ .export-model-actions-card {
2898
+ display: grid;
2899
+ gap: 14px;
2900
+ }
2901
+
2902
+ .export-model-fields {
2903
+ display: grid;
2904
+ grid-template-columns: minmax(240px, 1.1fr) minmax(260px, 1fr);
2905
+ gap: 14px;
2906
+ }
2907
+
2908
+ .export-model-field {
2909
+ display: grid;
2910
+ gap: 7px;
2911
+ min-width: 0;
2912
+ }
2913
+
2914
+ .export-model-field span {
2915
+ font-weight: 700;
2916
+ color: #394a5e;
2917
+ font-size: 0.88rem;
2918
+ }
2919
+
2920
+ .export-model-field input,
2921
+ .export-model-field select {
2922
+ width: 100%;
2923
+ }
2924
+
2925
+ .export-model-buttons {
2926
+ display: flex;
2927
+ flex-wrap: wrap;
2928
+ gap: 10px;
2929
+ }
2930
+
2931
+ .export-model-buttons button {
2932
+ min-height: 40px;
2933
+ padding-inline: 16px;
2934
+ }
2935
+
2936
+ .export-model-btn-primary {
2937
+ --btn-bg-start: #ff9b32;
2938
+ --btn-bg-end: #e67900;
2939
+ --btn-border: #d97300;
2940
+ --btn-shadow-soft: rgba(230, 121, 0, 0.18);
2941
+ --btn-shadow-strong: rgba(230, 121, 0, 0.28);
2942
+ }
2943
+
2944
+ .export-model-btn-repo {
2945
+ --btn-bg-start: #2ea94f;
2946
+ --btn-bg-end: #238a40;
2947
+ --btn-border: #1b7435;
2948
+ --btn-shadow-soft: rgba(35, 138, 64, 0.18);
2949
+ --btn-shadow-strong: rgba(35, 138, 64, 0.28);
2950
+ }
2951
+
2952
+ .export-model-btn-base {
2953
+ --btn-bg-start: #4e95cf;
2954
+ --btn-bg-end: #3d82be;
2955
+ --btn-border: #2e6d9f;
2956
+ --btn-shadow-soft: rgba(61, 130, 190, 0.18);
2957
+ --btn-shadow-strong: rgba(61, 130, 190, 0.28);
2958
+ }
2959
+
2960
+ .export-model-hint,
2961
+ .export-model-status {
2962
+ margin-top: 4px;
2963
+ }
2964
+
2965
+ .export-model-status {
2966
+ color: #1f5e3a;
2967
+ font-weight: 700;
2968
+ }
2969
+
2970
+ @media (max-width: 900px) {
2971
+ .export-model-fields {
2972
+ grid-template-columns: 1fr;
2973
+ }
2974
+
2975
+ .export-model-buttons button {
2976
+ flex: 1 1 100%;
2977
+ }
2978
+ }
2979
+
2980
  .section1-groups {
2981
  display: grid;
2982
  gap: 18px;
 
3309
  font-size: 0.83rem;
3310
  }
3311
 
3312
+ .modelo-observacao-badge {
3313
+ margin-top: 10px;
3314
+ }
3315
+
3316
+ .modelo-observacao-text {
3317
+ margin-top: 6px;
3318
+ color: #40586f;
3319
+ font-size: 0.88rem;
3320
+ line-height: 1.52;
3321
+ white-space: pre-line;
3322
+ }
3323
+
3324
  .modelo-variaveis-box {
3325
  padding: 9px 11px;
3326
  border-radius: 11px;