Guilherme Silberfarb Costa commited on
Commit
2a8204e
·
1 Parent(s): 8cc746d

introducao do repositorio

Browse files
README.md CHANGED
@@ -53,3 +53,23 @@ Para apontar para outro backend:
53
  ```bash
54
  VITE_API_BASE=http://localhost:8000 npm run dev
55
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  ```bash
54
  VITE_API_BASE=http://localhost:8000 npm run dev
55
  ```
56
+
57
+ ## Repositório de modelos `.dai`
58
+
59
+ Os modelos usados em **Pesquisa**, **Elaboração** (carregar modelo existente) e
60
+ **Visualização** (carregar modelo existente) podem vir de duas fontes:
61
+
62
+ - `local` (pasta do projeto)
63
+ - `hf_dataset` (dataset no Hugging Face)
64
+
65
+ Variáveis de ambiente do backend:
66
+
67
+ - `MODELOS_REPOSITORIO_PROVIDER` (`local` ou `hf_dataset`)
68
+ - `MODELOS_REPOSITORIO_LOCAL_DIR` (opcional, quando `local`)
69
+ - `MODELOS_REPOSITORIO_HF_REPO_ID` (ex.: `gui-sparim/repositorio_mesa`)
70
+ - `MODELOS_REPOSITORIO_HF_REVISION` (ex.: `main`)
71
+ - `MODELOS_REPOSITORIO_HF_SUBDIR` (ex.: `modelos_dai`)
72
+ - `HF_TOKEN` (opcional para dataset privado)
73
+
74
+ No modo `hf_dataset`, o backend consulta a revisão atual do dataset e só
75
+ sincroniza novamente quando detectar mudança de revisão.
backend/app/api/elaboracao.py CHANGED
@@ -128,6 +128,10 @@ class ColunaDataMercadoPayload(SessionPayload):
128
  coluna_data: str
129
 
130
 
 
 
 
 
131
  @router.post("/upload")
132
  async def upload_file(
133
  session_id: str = Form(...),
@@ -139,6 +143,17 @@ async def upload_file(
139
  return elaboracao_service.load_uploaded_file(session)
140
 
141
 
 
 
 
 
 
 
 
 
 
 
 
142
  @router.post("/confirm-sheet")
143
  def confirm_sheet(payload: ConfirmSheetPayload) -> dict[str, Any]:
144
  session = session_store.get(payload.session_id)
 
128
  coluna_data: str
129
 
130
 
131
+ class RepositorioModeloPayload(SessionPayload):
132
+ modelo_id: str
133
+
134
+
135
  @router.post("/upload")
136
  async def upload_file(
137
  session_id: str = Form(...),
 
143
  return elaboracao_service.load_uploaded_file(session)
144
 
145
 
146
+ @router.get("/repositorio-modelos")
147
+ def repositorio_modelos() -> dict[str, Any]:
148
+ return elaboracao_service.listar_modelos_repositorio()
149
+
150
+
151
+ @router.post("/repositorio-carregar")
152
+ def repositorio_carregar(payload: RepositorioModeloPayload) -> dict[str, Any]:
153
+ session = session_store.get(payload.session_id)
154
+ return elaboracao_service.carregar_modelo_repositorio(session, payload.modelo_id)
155
+
156
+
157
  @router.post("/confirm-sheet")
158
  def confirm_sheet(payload: ConfirmSheetPayload) -> dict[str, Any]:
159
  session = session_store.get(payload.session_id)
backend/app/api/visualizacao.py CHANGED
@@ -36,6 +36,10 @@ class AvaliacaoBasePayload(SessionPayload):
36
  indice_base: str | None = None
37
 
38
 
 
 
 
 
39
  @router.post("/upload")
40
  async def upload_file(
41
  session_id: str = Form(...),
@@ -47,6 +51,17 @@ async def upload_file(
47
  return visualizacao_service.carregar_modelo(session, caminho)
48
 
49
 
 
 
 
 
 
 
 
 
 
 
 
50
  @router.post("/exibir")
51
  def exibir(payload: SessionPayload) -> dict[str, Any]:
52
  session = session_store.get(payload.session_id)
 
36
  indice_base: str | None = None
37
 
38
 
39
+ class RepositorioModeloPayload(SessionPayload):
40
+ modelo_id: str
41
+
42
+
43
  @router.post("/upload")
44
  async def upload_file(
45
  session_id: str = Form(...),
 
51
  return visualizacao_service.carregar_modelo(session, caminho)
52
 
53
 
54
+ @router.get("/repositorio-modelos")
55
+ def repositorio_modelos() -> dict[str, Any]:
56
+ return visualizacao_service.listar_modelos_repositorio()
57
+
58
+
59
+ @router.post("/repositorio-carregar")
60
+ def repositorio_carregar(payload: RepositorioModeloPayload) -> dict[str, Any]:
61
+ session = session_store.get(payload.session_id)
62
+ return visualizacao_service.carregar_modelo_repositorio(session, payload.modelo_id)
63
+
64
+
65
  @router.post("/exibir")
66
  def exibir(payload: SessionPayload) -> dict[str, Any]:
67
  session = session_store.get(payload.session_id)
backend/app/core/pesquisa/modelos_dai/README.md CHANGED
@@ -1,5 +1,8 @@
1
  # Pasta de Modelos da Aba Pesquisa
2
 
 
 
 
3
  Coloque nesta pasta os arquivos `.dai` que devem aparecer na aba **Pesquisa**.
4
 
5
  ## Estrutura
 
1
  # Pasta de Modelos da Aba Pesquisa
2
 
3
+ Esta pasta é usada quando o provider de repositório de modelos está configurado
4
+ como `local` (`MODELOS_REPOSITORIO_PROVIDER=local`).
5
+
6
  Coloque nesta pasta os arquivos `.dai` que devem aparecer na aba **Pesquisa**.
7
 
8
  ## Estrutura
backend/app/services/elaboracao_service.py CHANGED
@@ -40,6 +40,7 @@ from app.core.elaboracao.formatadores import (
40
  formatar_outliers_anteriores_html,
41
  )
42
  from app.models.session import SessionState
 
43
  from app.services.serializers import dataframe_to_payload, figure_to_payload, sanitize_value
44
 
45
  _AVALIADORES_PATH = Path(__file__).resolve().parent.parent / "core" / "elaboracao" / "avaliadores.json"
@@ -472,6 +473,17 @@ def load_uploaded_file(session: SessionState, selected_sheet: str | None = None)
472
  }
473
 
474
 
 
 
 
 
 
 
 
 
 
 
 
475
  def load_dai_for_elaboracao(session: SessionState, caminho_arquivo: str) -> dict[str, Any]:
476
  (
477
  df,
 
40
  formatar_outliers_anteriores_html,
41
  )
42
  from app.models.session import SessionState
43
+ from app.services import model_repository
44
  from app.services.serializers import dataframe_to_payload, figure_to_payload, sanitize_value
45
 
46
  _AVALIADORES_PATH = Path(__file__).resolve().parent.parent / "core" / "elaboracao" / "avaliadores.json"
 
473
  }
474
 
475
 
476
+ def listar_modelos_repositorio() -> dict[str, Any]:
477
+ return sanitize_value(model_repository.list_repository_models())
478
+
479
+
480
+ def carregar_modelo_repositorio(session: SessionState, modelo_id: str) -> dict[str, Any]:
481
+ caminho = model_repository.resolve_model_file(modelo_id)
482
+ session.uploaded_file_path = str(caminho)
483
+ session.uploaded_filename = caminho.name
484
+ return load_dai_for_elaboracao(session, str(caminho))
485
+
486
+
487
  def load_dai_for_elaboracao(session: SessionState, caminho_arquivo: str) -> dict[str, Any]:
488
  (
489
  df,
backend/app/services/model_repository.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from threading import Lock
7
+ from typing import Any
8
+
9
+ from fastapi import HTTPException
10
+
11
+ try:
12
+ from huggingface_hub import HfApi, snapshot_download
13
+ except Exception: # pragma: no cover - dependência opcional em tempo de import
14
+ HfApi = None # type: ignore[assignment]
15
+ snapshot_download = None # type: ignore[assignment]
16
+
17
+
18
+ DEFAULT_LOCAL_MODELOS_DIR = Path(__file__).resolve().parent.parent / "core" / "pesquisa" / "modelos_dai"
19
+ DEFAULT_HF_REPO_ID = "gui-sparim/repositorio_mesa"
20
+ DEFAULT_HF_REVISION = "main"
21
+ DEFAULT_HF_SUBDIR = "modelos_dai"
22
+
23
+ _STATE_LOCK = Lock()
24
+ _STATE: dict[str, Any] = {
25
+ "provider": None,
26
+ "signature": None,
27
+ "revision": None,
28
+ "repo_id": None,
29
+ "subdir": None,
30
+ "modelos_dir": None,
31
+ }
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class ModelRepositoryResolution:
36
+ provider: str
37
+ modelos_dir: Path
38
+ signature: str
39
+ revision: str | None = None
40
+ repo_id: str | None = None
41
+ subdir: str | None = None
42
+ degraded: bool = False
43
+
44
+ def as_payload(self) -> dict[str, Any]:
45
+ return {
46
+ "provider": self.provider,
47
+ "revision": self.revision,
48
+ "repo_id": self.repo_id,
49
+ "subdir": self.subdir,
50
+ "degraded": self.degraded,
51
+ "modelos_dir": str(self.modelos_dir),
52
+ "signature": self.signature,
53
+ }
54
+
55
+
56
+ def _provider() -> str:
57
+ raw = (
58
+ os.getenv("MODELOS_REPOSITORIO_PROVIDER")
59
+ or os.getenv("PESQUISA_MODELOS_PROVIDER")
60
+ or "local"
61
+ )
62
+ value = str(raw).strip().lower()
63
+ if value in {"hf", "hf_dataset", "dataset", "huggingface"}:
64
+ return "hf_dataset"
65
+ return "local"
66
+
67
+
68
+ def _hf_repo_id() -> str:
69
+ return str(
70
+ os.getenv("MODELOS_REPOSITORIO_HF_REPO_ID")
71
+ or os.getenv("PESQUISA_HF_REPO_ID")
72
+ or DEFAULT_HF_REPO_ID
73
+ ).strip()
74
+
75
+
76
+ def _hf_revision() -> str:
77
+ return str(
78
+ os.getenv("MODELOS_REPOSITORIO_HF_REVISION")
79
+ or os.getenv("PESQUISA_HF_REVISION")
80
+ or DEFAULT_HF_REVISION
81
+ ).strip()
82
+
83
+
84
+ def _hf_subdir() -> str:
85
+ return str(
86
+ os.getenv("MODELOS_REPOSITORIO_HF_SUBDIR")
87
+ or os.getenv("PESQUISA_HF_SUBDIR")
88
+ or DEFAULT_HF_SUBDIR
89
+ ).strip().strip("/")
90
+
91
+
92
+ def _hf_token() -> str | None:
93
+ for key in ("HF_TOKEN", "HUGGINGFACE_HUB_TOKEN", "HUGGINGFACE_TOKEN"):
94
+ value = os.getenv(key)
95
+ if value:
96
+ return value
97
+ return None
98
+
99
+
100
+ def _local_mode_models_dir() -> Path:
101
+ raw = os.getenv("MODELOS_REPOSITORIO_LOCAL_DIR")
102
+ if raw and str(raw).strip():
103
+ return Path(str(raw).strip()).expanduser().resolve()
104
+ return DEFAULT_LOCAL_MODELOS_DIR
105
+
106
+
107
+ def _ensure_local_resolution() -> ModelRepositoryResolution:
108
+ modelos_dir = _local_mode_models_dir()
109
+ modelos_dir.mkdir(parents=True, exist_ok=True)
110
+ signature = f"local:{modelos_dir}"
111
+ return ModelRepositoryResolution(
112
+ provider="local",
113
+ modelos_dir=modelos_dir,
114
+ signature=signature,
115
+ )
116
+
117
+
118
+ def _resolve_hf() -> ModelRepositoryResolution:
119
+ if HfApi is None or snapshot_download is None:
120
+ raise HTTPException(
121
+ status_code=500,
122
+ detail="Provider hf_dataset indisponivel: instale huggingface_hub no backend",
123
+ )
124
+
125
+ repo_id = _hf_repo_id()
126
+ revision_ref = _hf_revision()
127
+ subdir = _hf_subdir()
128
+ token = _hf_token()
129
+ pattern = f"{subdir}/*.dai"
130
+
131
+ api = HfApi(token=token)
132
+
133
+ try:
134
+ info = api.dataset_info(repo_id=repo_id, revision=revision_ref, token=token)
135
+ except Exception as exc:
136
+ with _STATE_LOCK:
137
+ snapshot_dir = _STATE.get("modelos_dir")
138
+ signature = _STATE.get("signature")
139
+ rev = _STATE.get("revision")
140
+ same_repo = _STATE.get("provider") == "hf_dataset" and _STATE.get("repo_id") == repo_id
141
+ if snapshot_dir and signature and same_repo and Path(snapshot_dir).exists():
142
+ return ModelRepositoryResolution(
143
+ provider="hf_dataset",
144
+ modelos_dir=Path(snapshot_dir),
145
+ signature=str(signature),
146
+ revision=str(rev) if rev else None,
147
+ repo_id=repo_id,
148
+ subdir=subdir,
149
+ degraded=True,
150
+ )
151
+ raise HTTPException(
152
+ status_code=503,
153
+ detail=f"Nao foi possivel consultar o repositório de modelos no Hugging Face: {exc}",
154
+ ) from exc
155
+
156
+ commit_sha = str(getattr(info, "sha", "") or "").strip() or revision_ref
157
+ signature = f"hf_dataset:{repo_id}@{commit_sha}:{subdir}"
158
+
159
+ with _STATE_LOCK:
160
+ current_signature = _STATE.get("signature")
161
+ current_dir = _STATE.get("modelos_dir")
162
+ if (
163
+ current_signature == signature
164
+ and current_dir
165
+ and Path(str(current_dir)).exists()
166
+ ):
167
+ return ModelRepositoryResolution(
168
+ provider="hf_dataset",
169
+ modelos_dir=Path(str(current_dir)),
170
+ signature=signature,
171
+ revision=commit_sha,
172
+ repo_id=repo_id,
173
+ subdir=subdir,
174
+ )
175
+
176
+ try:
177
+ snapshot_root = Path(
178
+ snapshot_download(
179
+ repo_id=repo_id,
180
+ repo_type="dataset",
181
+ revision=commit_sha,
182
+ allow_patterns=[pattern],
183
+ token=token,
184
+ )
185
+ )
186
+ modelos_dir = snapshot_root / subdir
187
+ if not modelos_dir.exists():
188
+ raise RuntimeError(f"Pasta '{subdir}' nao encontrada no snapshot '{commit_sha}'")
189
+ except Exception as exc:
190
+ with _STATE_LOCK:
191
+ fallback_dir = _STATE.get("modelos_dir")
192
+ fallback_signature = _STATE.get("signature")
193
+ fallback_rev = _STATE.get("revision")
194
+ same_repo = _STATE.get("provider") == "hf_dataset" and _STATE.get("repo_id") == repo_id
195
+ if fallback_dir and fallback_signature and same_repo and Path(str(fallback_dir)).exists():
196
+ return ModelRepositoryResolution(
197
+ provider="hf_dataset",
198
+ modelos_dir=Path(str(fallback_dir)),
199
+ signature=str(fallback_signature),
200
+ revision=str(fallback_rev) if fallback_rev else None,
201
+ repo_id=repo_id,
202
+ subdir=subdir,
203
+ degraded=True,
204
+ )
205
+ raise HTTPException(
206
+ status_code=503,
207
+ detail=f"Nao foi possivel sincronizar modelos do dataset no Hugging Face: {exc}",
208
+ ) from exc
209
+
210
+ with _STATE_LOCK:
211
+ _STATE.update(
212
+ {
213
+ "provider": "hf_dataset",
214
+ "signature": signature,
215
+ "revision": commit_sha,
216
+ "repo_id": repo_id,
217
+ "subdir": subdir,
218
+ "modelos_dir": str(modelos_dir),
219
+ }
220
+ )
221
+
222
+ return ModelRepositoryResolution(
223
+ provider="hf_dataset",
224
+ modelos_dir=modelos_dir,
225
+ signature=signature,
226
+ revision=commit_sha,
227
+ repo_id=repo_id,
228
+ subdir=subdir,
229
+ )
230
+
231
+
232
+ def resolve_model_repository() -> ModelRepositoryResolution:
233
+ provider = _provider()
234
+ if provider == "hf_dataset":
235
+ return _resolve_hf()
236
+ return _ensure_local_resolution()
237
+
238
+
239
+ def list_repository_models() -> dict[str, Any]:
240
+ resolved = resolve_model_repository()
241
+ modelos = sorted(resolved.modelos_dir.glob("*.dai"), key=lambda item: item.name.lower())
242
+ return {
243
+ "modelos": [
244
+ {
245
+ "id": caminho.stem,
246
+ "arquivo": caminho.name,
247
+ "nome_modelo": caminho.stem,
248
+ }
249
+ for caminho in modelos
250
+ ],
251
+ "total_modelos": len(modelos),
252
+ "fonte": resolved.as_payload(),
253
+ }
254
+
255
+
256
+ def resolve_model_file(modelo_id: str) -> Path:
257
+ resolved = resolve_model_repository()
258
+ chave = str(modelo_id or "").strip()
259
+ if not chave:
260
+ raise HTTPException(status_code=400, detail="Informe o identificador do modelo")
261
+
262
+ modelos = sorted(resolved.modelos_dir.glob("*.dai"), key=lambda item: item.name.lower())
263
+ by_stem = {caminho.stem.lower(): caminho for caminho in modelos}
264
+ by_name = {caminho.name.lower(): caminho for caminho in modelos}
265
+
266
+ candidato = by_stem.get(chave.lower()) or by_name.get(chave.lower())
267
+ if candidato is None and not chave.lower().endswith(".dai"):
268
+ candidato = by_name.get(f"{chave.lower()}.dai")
269
+ if candidato is None:
270
+ raise HTTPException(status_code=404, detail="Modelo nao encontrado no repositório configurado")
271
+ return candidato
backend/app/services/pesquisa_service.py CHANGED
@@ -17,9 +17,9 @@ from fastapi import HTTPException
17
  from joblib import load
18
 
19
  from app.core.elaboracao.core import _migrar_pacote_v1_para_v2
 
20
  from app.services.serializers import sanitize_value
21
 
22
- MODELOS_DIR = Path(__file__).resolve().parent.parent / "core" / "pesquisa" / "modelos_dai"
23
  ADMIN_CONFIG_PATH = Path(__file__).resolve().parent.parent / "core" / "pesquisa" / "pesquisa_admin_config.json"
24
 
25
  AREA_PRIVATIVA_ALIASES = ["APRIV", "APRIVEQ", "ATPRIV", "ACOPRIV", "AREAPRIV", "AREA_PRIVATIVA", "AREA PRIVATIVA"]
@@ -192,15 +192,26 @@ class PesquisaFiltros:
192
  _CACHE_LOCK = Lock()
193
  _CACHE: dict[str, dict[str, Any]] = {}
194
  _ADMIN_CONFIG_LOCK = Lock()
 
 
 
 
 
 
 
 
 
 
 
195
 
196
 
197
  def ensure_modelos_dir() -> Path:
198
- MODELOS_DIR.mkdir(parents=True, exist_ok=True)
199
- return MODELOS_DIR
200
 
201
 
202
  def obter_admin_config_pesquisa() -> dict[str, Any]:
203
- pasta = ensure_modelos_dir()
 
204
  modelos = sorted(pasta.glob("*.dai"), key=lambda item: item.name.lower())
205
  todos = [_carregar_resumo_com_cache(caminho) for caminho in modelos]
206
  colunas_filtro = _montar_config_colunas_filtro(todos)
@@ -210,12 +221,14 @@ def obter_admin_config_pesquisa() -> dict[str, Any]:
210
  "colunas_filtro": colunas_filtro,
211
  "admin_fontes": admin_fontes,
212
  "total_modelos": len(todos),
 
213
  }
214
  )
215
 
216
 
217
  def salvar_admin_config_pesquisa(campos: dict[str, list[str]] | None) -> dict[str, Any]:
218
- pasta = ensure_modelos_dir()
 
219
  modelos = sorted(pasta.glob("*.dai"), key=lambda item: item.name.lower())
220
  todos = [_carregar_resumo_com_cache(caminho) for caminho in modelos]
221
  colunas_filtro = _montar_config_colunas_filtro(todos)
@@ -227,12 +240,14 @@ def salvar_admin_config_pesquisa(campos: dict[str, list[str]] | None) -> dict[st
227
  "admin_fontes": admin_fontes,
228
  "total_modelos": len(todos),
229
  "status": "Configuracao de busca salva com sucesso.",
 
230
  }
231
  )
232
 
233
 
234
  def listar_modelos(filtros: PesquisaFiltros, limite: int | None = None, somente_contexto: bool = False) -> dict[str, Any]:
235
- pasta = ensure_modelos_dir()
 
236
  modelos = sorted(pasta.glob("*.dai"), key=lambda item: item.name.lower())
237
 
238
  otica = _normalizar_otica(filtros.otica)
@@ -252,6 +267,7 @@ def listar_modelos(filtros: PesquisaFiltros, limite: int | None = None, somente_
252
  "total_filtrado": 0,
253
  "total_geral": len(todos),
254
  "modelos_dir": str(pasta),
 
255
  "admin_fontes": admin_fontes,
256
  "filtros_aplicados": {
257
  "nome": filtros.nome,
@@ -316,6 +332,7 @@ def listar_modelos(filtros: PesquisaFiltros, limite: int | None = None, somente_
316
  "total_filtrado": len(filtrados),
317
  "total_geral": len(todos),
318
  "modelos_dir": str(pasta),
 
319
  "admin_fontes": admin_fontes,
320
  "filtros_aplicados": {
321
  "nome": filtros.nome,
 
17
  from joblib import load
18
 
19
  from app.core.elaboracao.core import _migrar_pacote_v1_para_v2
20
+ from app.services import model_repository
21
  from app.services.serializers import sanitize_value
22
 
 
23
  ADMIN_CONFIG_PATH = Path(__file__).resolve().parent.parent / "core" / "pesquisa" / "pesquisa_admin_config.json"
24
 
25
  AREA_PRIVATIVA_ALIASES = ["APRIV", "APRIVEQ", "ATPRIV", "ACOPRIV", "AREAPRIV", "AREA_PRIVATIVA", "AREA PRIVATIVA"]
 
192
  _CACHE_LOCK = Lock()
193
  _CACHE: dict[str, dict[str, Any]] = {}
194
  _ADMIN_CONFIG_LOCK = Lock()
195
+ _CACHE_SOURCE_SIGNATURE: str | None = None
196
+
197
+
198
+ def _resolver_repositorio_modelos() -> model_repository.ModelRepositoryResolution:
199
+ global _CACHE_SOURCE_SIGNATURE
200
+ resolved = model_repository.resolve_model_repository()
201
+ with _CACHE_LOCK:
202
+ if _CACHE_SOURCE_SIGNATURE != resolved.signature:
203
+ _CACHE.clear()
204
+ _CACHE_SOURCE_SIGNATURE = resolved.signature
205
+ return resolved
206
 
207
 
208
  def ensure_modelos_dir() -> Path:
209
+ return _resolver_repositorio_modelos().modelos_dir
 
210
 
211
 
212
  def obter_admin_config_pesquisa() -> dict[str, Any]:
213
+ resolved = _resolver_repositorio_modelos()
214
+ pasta = resolved.modelos_dir
215
  modelos = sorted(pasta.glob("*.dai"), key=lambda item: item.name.lower())
216
  todos = [_carregar_resumo_com_cache(caminho) for caminho in modelos]
217
  colunas_filtro = _montar_config_colunas_filtro(todos)
 
221
  "colunas_filtro": colunas_filtro,
222
  "admin_fontes": admin_fontes,
223
  "total_modelos": len(todos),
224
+ "fonte_modelos": resolved.as_payload(),
225
  }
226
  )
227
 
228
 
229
  def salvar_admin_config_pesquisa(campos: dict[str, list[str]] | None) -> dict[str, Any]:
230
+ resolved = _resolver_repositorio_modelos()
231
+ pasta = resolved.modelos_dir
232
  modelos = sorted(pasta.glob("*.dai"), key=lambda item: item.name.lower())
233
  todos = [_carregar_resumo_com_cache(caminho) for caminho in modelos]
234
  colunas_filtro = _montar_config_colunas_filtro(todos)
 
240
  "admin_fontes": admin_fontes,
241
  "total_modelos": len(todos),
242
  "status": "Configuracao de busca salva com sucesso.",
243
+ "fonte_modelos": resolved.as_payload(),
244
  }
245
  )
246
 
247
 
248
  def listar_modelos(filtros: PesquisaFiltros, limite: int | None = None, somente_contexto: bool = False) -> dict[str, Any]:
249
+ resolved = _resolver_repositorio_modelos()
250
+ pasta = resolved.modelos_dir
251
  modelos = sorted(pasta.glob("*.dai"), key=lambda item: item.name.lower())
252
 
253
  otica = _normalizar_otica(filtros.otica)
 
267
  "total_filtrado": 0,
268
  "total_geral": len(todos),
269
  "modelos_dir": str(pasta),
270
+ "fonte_modelos": resolved.as_payload(),
271
  "admin_fontes": admin_fontes,
272
  "filtros_aplicados": {
273
  "nome": filtros.nome,
 
332
  "total_filtrado": len(filtrados),
333
  "total_geral": len(todos),
334
  "modelos_dir": str(pasta),
335
+ "fonte_modelos": resolved.as_payload(),
336
  "admin_fontes": admin_fontes,
337
  "filtros_aplicados": {
338
  "nome": filtros.nome,
backend/app/services/visualizacao_service.py CHANGED
@@ -12,12 +12,24 @@ from app.core.visualizacao import app as viz_app
12
  from app.core.elaboracao.core import _migrar_pacote_v1_para_v2, avaliar_imovel, exportar_avaliacoes_excel
13
  from app.core.elaboracao.formatadores import formatar_avaliacao_html
14
  from app.models.session import SessionState
 
15
  from app.services.serializers import dataframe_to_payload, figure_to_payload, sanitize_value
16
 
17
 
18
  CHAVES_ESPERADAS = ["versao", "dados", "transformacoes", "modelo"]
19
 
20
 
 
 
 
 
 
 
 
 
 
 
 
21
  def carregar_modelo(session: SessionState, caminho_arquivo: str) -> dict[str, Any]:
22
  try:
23
  pacote = load(caminho_arquivo)
 
12
  from app.core.elaboracao.core import _migrar_pacote_v1_para_v2, avaliar_imovel, exportar_avaliacoes_excel
13
  from app.core.elaboracao.formatadores import formatar_avaliacao_html
14
  from app.models.session import SessionState
15
+ from app.services import model_repository
16
  from app.services.serializers import dataframe_to_payload, figure_to_payload, sanitize_value
17
 
18
 
19
  CHAVES_ESPERADAS = ["versao", "dados", "transformacoes", "modelo"]
20
 
21
 
22
+ def listar_modelos_repositorio() -> dict[str, Any]:
23
+ return sanitize_value(model_repository.list_repository_models())
24
+
25
+
26
+ def carregar_modelo_repositorio(session: SessionState, modelo_id: str) -> dict[str, Any]:
27
+ caminho = model_repository.resolve_model_file(modelo_id)
28
+ session.uploaded_file_path = str(caminho)
29
+ session.uploaded_filename = caminho.name
30
+ return carregar_modelo(session, str(caminho))
31
+
32
+
33
  def carregar_modelo(session: SessionState, caminho_arquivo: str) -> dict[str, Any]:
34
  try:
35
  pacote = load(caminho_arquivo)
backend/requirements.txt CHANGED
@@ -14,3 +14,4 @@ openpyxl
14
  geopandas
15
  fiona
16
  gradio>=4.0
 
 
14
  geopandas
15
  fiona
16
  gradio>=4.0
17
+ huggingface_hub>=0.30.0
frontend/src/api.js CHANGED
@@ -83,6 +83,8 @@ export const api = {
83
  form.append('file', file)
84
  return postForm('/api/elaboracao/upload', form)
85
  },
 
 
86
 
87
  confirmSheet: (sessionId, sheetName) => postJson('/api/elaboracao/confirm-sheet', { session_id: sessionId, sheet_name: sheetName }),
88
  mapCoords: (sessionId, colLat, colLon) => postJson('/api/elaboracao/map-coords', { session_id: sessionId, col_lat: colLat, col_lon: colLon }),
@@ -164,6 +166,8 @@ export const api = {
164
  form.append('file', file)
165
  return postForm('/api/visualizacao/upload', form)
166
  },
 
 
167
  exibirVisualizacao: (sessionId) => postJson('/api/visualizacao/exibir', { session_id: sessionId }),
168
  updateVisualizacaoMap: (sessionId, variavelMapa) => postJson('/api/visualizacao/map/update', { session_id: sessionId, variavel_mapa: variavelMapa }),
169
  evaluationFieldsViz: (sessionId) => postJson('/api/visualizacao/evaluation/fields', { session_id: sessionId }),
 
83
  form.append('file', file)
84
  return postForm('/api/elaboracao/upload', form)
85
  },
86
+ elaboracaoRepositorioModelos: () => getJson('/api/elaboracao/repositorio-modelos'),
87
+ elaboracaoRepositorioCarregar: (sessionId, modeloId) => postJson('/api/elaboracao/repositorio-carregar', { session_id: sessionId, modelo_id: modeloId }),
88
 
89
  confirmSheet: (sessionId, sheetName) => postJson('/api/elaboracao/confirm-sheet', { session_id: sessionId, sheet_name: sheetName }),
90
  mapCoords: (sessionId, colLat, colLon) => postJson('/api/elaboracao/map-coords', { session_id: sessionId, col_lat: colLat, col_lon: colLon }),
 
166
  form.append('file', file)
167
  return postForm('/api/visualizacao/upload', form)
168
  },
169
+ visualizacaoRepositorioModelos: () => getJson('/api/visualizacao/repositorio-modelos'),
170
+ visualizacaoRepositorioCarregar: (sessionId, modeloId) => postJson('/api/visualizacao/repositorio-carregar', { session_id: sessionId, modelo_id: modeloId }),
171
  exibirVisualizacao: (sessionId) => postJson('/api/visualizacao/exibir', { session_id: sessionId }),
172
  updateVisualizacaoMap: (sessionId, variavelMapa) => postJson('/api/visualizacao/map/update', { session_id: sessionId, variavel_mapa: variavelMapa }),
173
  evaluationFieldsViz: (sessionId) => postJson('/api/visualizacao/evaluation/fields', { session_id: sessionId }),
frontend/src/components/ElaboracaoTab.jsx CHANGED
@@ -544,6 +544,10 @@ export default function ElaboracaoTab({ sessionId }) {
544
  const [selectedSheet, setSelectedSheet] = useState('')
545
  const [elaborador, setElaborador] = useState(null)
546
  const [modeloCarregadoInfo, setModeloCarregadoInfo] = useState(null)
 
 
 
 
547
 
548
  const [dados, setDados] = useState(null)
549
  const [mapaHtml, setMapaHtml] = useState('')
@@ -953,6 +957,34 @@ export default function ElaboracaoTab({ sessionId }) {
953
  }
954
  }, [sessionId])
955
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
956
  async function withBusy(fn) {
957
  setLoading(true)
958
  setError('')
@@ -980,6 +1012,45 @@ export default function ElaboracaoTab({ sessionId }) {
980
  })
981
  }
982
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
983
  function applyBaseResponse(resp, options = {}) {
984
  const resetXSelection = Boolean(options.resetXSelection)
985
  const colunaYPadrao = String(resp.coluna_y_padrao || '')
@@ -1169,6 +1240,70 @@ export default function ElaboracaoTab({ sessionId }) {
1169
  setBaseValue('')
1170
  }
1171
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1172
  async function onUploadClick(arquivo = null) {
1173
  const arquivoUpload = arquivo || uploadedFile
1174
  if (!arquivoUpload || !sessionId) return
@@ -1180,58 +1315,20 @@ export default function ElaboracaoTab({ sessionId }) {
1180
  const uploadEhDai = nomeArquivo.endsWith('.dai')
1181
  setTipoFonteDados(uploadEhDai ? 'dai' : 'tabular')
1182
  const resp = await api.uploadElaboracaoFile(sessionId, arquivoUpload)
1183
- setManualMapError('')
1184
- setGeoProcessError('')
1185
- setGeoStatusHtml('')
1186
- setGeoFalhasHtml('')
1187
- setGeoCorrecoes([])
1188
- setElaborador(resp.elaborador || null)
1189
- setModeloCarregadoInfo(buildLoadedModelInfo(resp))
1190
- setAvaliadorSelecionado(resp.elaborador?.nome_completo || '')
1191
- setRequiresSheet(Boolean(resp.requires_sheet))
1192
- setSheetOptions(resp.sheets || [])
1193
- setSelectedSheet(resp.sheet_selected || '')
1194
- if (!resp.requires_sheet) {
1195
- const origemResp = String(resp.tipo || '').toLowerCase()
1196
- setTipoFonteDados(origemResp === 'dai' ? 'dai' : 'tabular')
1197
- const resetXSelection = String(resp.tipo || '').toLowerCase() !== 'dai'
1198
- applyBaseResponse(resp, { resetXSelection })
1199
- } else if (resp.status) {
1200
- setTipoFonteDados('tabular')
1201
- setColunaY('')
1202
- setColunaYDraft('')
1203
- setSelection(null)
1204
- setFit(null)
1205
- setSelectionAppliedSnapshot(buildSelectionSnapshot())
1206
- setColunasX([])
1207
- setDicotomicas([])
1208
- setCodigoAlocado([])
1209
- setPercentuais([])
1210
- setTransformacaoY('(x)')
1211
- setTransformacoesX({})
1212
- setTransformacoesAplicadas(null)
1213
- setOrigemTransformacoes(null)
1214
- setBuscaTransformAppliedSnapshot(buildGrauSnapshot(0, 0))
1215
- setManualTransformAppliedSnapshot(buildTransformacoesSnapshot('(x)', {}))
1216
- setColunasDataMercado([])
1217
- setColunaDataMercadoSugerida('')
1218
- setColunaDataMercado('')
1219
- setColunaDataMercadoAplicada('')
1220
- setPeriodoDadosMercado(null)
1221
- setPeriodoDadosMercadoPreview(null)
1222
- setDataMercadoError('')
1223
- setFiltros(defaultFiltros())
1224
- setOutliersTexto('')
1225
- setReincluirTexto('')
1226
- setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
1227
- setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
1228
- setCamposAvaliacao([])
1229
- valoresAvaliacaoRef.current = {}
1230
- setAvaliacaoFormVersion((prev) => prev + 1)
1231
- setAvaliacaoPendente(false)
1232
- setResultadoAvaliacaoHtml('')
1233
- setStatus(resp.status)
1234
- }
1235
  })
1236
  }
1237
 
@@ -1963,6 +2060,32 @@ export default function ElaboracaoTab({ sessionId }) {
1963
  <div className="section1-groups">
1964
  <div className="subpanel section1-group">
1965
  <h4>Carregar modelo</h4>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1966
  <div
1967
  className={`upload-dropzone${uploadDragOver ? ' is-dragover' : ''}`}
1968
  onDragOver={onUploadDropZoneDragOver}
 
544
  const [selectedSheet, setSelectedSheet] = useState('')
545
  const [elaborador, setElaborador] = useState(null)
546
  const [modeloCarregadoInfo, setModeloCarregadoInfo] = useState(null)
547
+ const [repoModelos, setRepoModelos] = useState([])
548
+ const [repoModeloSelecionado, setRepoModeloSelecionado] = useState('')
549
+ const [repoModelosLoading, setRepoModelosLoading] = useState(false)
550
+ const [repoFonteModelos, setRepoFonteModelos] = useState('')
551
 
552
  const [dados, setDados] = useState(null)
553
  const [mapaHtml, setMapaHtml] = useState('')
 
957
  }
958
  }, [sessionId])
959
 
960
+ useEffect(() => {
961
+ let ativo = true
962
+ if (!sessionId) return () => {
963
+ ativo = false
964
+ }
965
+
966
+ setRepoModelosLoading(true)
967
+ api.elaboracaoRepositorioModelos()
968
+ .then((resp) => {
969
+ if (!ativo) return
970
+ aplicarRespostaModelosRepositorio(resp)
971
+ })
972
+ .catch(() => {
973
+ if (!ativo) return
974
+ setRepoModelos([])
975
+ setRepoModeloSelecionado('')
976
+ setRepoFonteModelos('')
977
+ })
978
+ .finally(() => {
979
+ if (!ativo) return
980
+ setRepoModelosLoading(false)
981
+ })
982
+
983
+ return () => {
984
+ ativo = false
985
+ }
986
+ }, [sessionId])
987
+
988
  async function withBusy(fn) {
989
  setLoading(true)
990
  setError('')
 
1012
  })
1013
  }
1014
 
1015
+ function formatarFonteRepositorio(fonte) {
1016
+ if (!fonte || typeof fonte !== 'object') return ''
1017
+ const provider = String(fonte.provider || '').toLowerCase()
1018
+ if (provider === 'hf_dataset') {
1019
+ const repo = String(fonte.repo_id || '').trim()
1020
+ const revision = String(fonte.revision || '').trim()
1021
+ const degradado = Boolean(fonte.degraded)
1022
+ const sufixo = degradado ? ' (modo contingência)' : ''
1023
+ return `Fonte: HF Dataset${repo ? ` (${repo})` : ''}${revision ? ` | revisão ${revision.slice(0, 8)}` : ''}${sufixo}`
1024
+ }
1025
+ return 'Fonte: pasta local'
1026
+ }
1027
+
1028
+ function aplicarRespostaModelosRepositorio(resp) {
1029
+ const modelos = Array.isArray(resp?.modelos) ? resp.modelos : []
1030
+ setRepoModelos(modelos)
1031
+ setRepoFonteModelos(formatarFonteRepositorio(resp?.fonte || null))
1032
+ setRepoModeloSelecionado((prev) => {
1033
+ const atual = String(prev || '')
1034
+ if (atual && modelos.some((item) => String(item.id) === atual)) return atual
1035
+ return String(modelos[0]?.id || '')
1036
+ })
1037
+ }
1038
+
1039
+ async function carregarModelosRepositorio() {
1040
+ setRepoModelosLoading(true)
1041
+ try {
1042
+ const resp = await api.elaboracaoRepositorioModelos()
1043
+ aplicarRespostaModelosRepositorio(resp)
1044
+ } catch (err) {
1045
+ setError(err.message || 'Falha ao carregar modelos do repositório.')
1046
+ setRepoModelos([])
1047
+ setRepoModeloSelecionado('')
1048
+ setRepoFonteModelos('')
1049
+ } finally {
1050
+ setRepoModelosLoading(false)
1051
+ }
1052
+ }
1053
+
1054
  function applyBaseResponse(resp, options = {}) {
1055
  const resetXSelection = Boolean(options.resetXSelection)
1056
  const colunaYPadrao = String(resp.coluna_y_padrao || '')
 
1240
  setBaseValue('')
1241
  }
1242
 
1243
+ function aplicarRespostaCarregamento(resp, tipoFonteFallback = 'tabular') {
1244
+ setManualMapError('')
1245
+ setGeoProcessError('')
1246
+ setGeoStatusHtml('')
1247
+ setGeoFalhasHtml('')
1248
+ setGeoCorrecoes([])
1249
+ setElaborador(resp.elaborador || null)
1250
+ setModeloCarregadoInfo(buildLoadedModelInfo(resp))
1251
+ setAvaliadorSelecionado(resp.elaborador?.nome_completo || '')
1252
+ setRequiresSheet(Boolean(resp.requires_sheet))
1253
+ setSheetOptions(resp.sheets || [])
1254
+ setSelectedSheet(resp.sheet_selected || '')
1255
+
1256
+ if (!resp.requires_sheet) {
1257
+ const origemResp = String(resp.tipo || '').toLowerCase()
1258
+ const tipoFonte = origemResp === 'dai'
1259
+ ? 'dai'
1260
+ : String(tipoFonteFallback || '').toLowerCase() === 'dai'
1261
+ ? 'dai'
1262
+ : 'tabular'
1263
+ setTipoFonteDados(tipoFonte)
1264
+ const resetXSelection = String(resp.tipo || '').toLowerCase() !== 'dai'
1265
+ applyBaseResponse(resp, { resetXSelection })
1266
+ return
1267
+ }
1268
+
1269
+ if (resp.status) {
1270
+ setTipoFonteDados('tabular')
1271
+ setColunaY('')
1272
+ setColunaYDraft('')
1273
+ setSelection(null)
1274
+ setFit(null)
1275
+ setSelectionAppliedSnapshot(buildSelectionSnapshot())
1276
+ setColunasX([])
1277
+ setDicotomicas([])
1278
+ setCodigoAlocado([])
1279
+ setPercentuais([])
1280
+ setTransformacaoY('(x)')
1281
+ setTransformacoesX({})
1282
+ setTransformacoesAplicadas(null)
1283
+ setOrigemTransformacoes(null)
1284
+ setBuscaTransformAppliedSnapshot(buildGrauSnapshot(0, 0))
1285
+ setManualTransformAppliedSnapshot(buildTransformacoesSnapshot('(x)', {}))
1286
+ setColunasDataMercado([])
1287
+ setColunaDataMercadoSugerida('')
1288
+ setColunaDataMercado('')
1289
+ setColunaDataMercadoAplicada('')
1290
+ setPeriodoDadosMercado(null)
1291
+ setPeriodoDadosMercadoPreview(null)
1292
+ setDataMercadoError('')
1293
+ setFiltros(defaultFiltros())
1294
+ setOutliersTexto('')
1295
+ setReincluirTexto('')
1296
+ setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
1297
+ setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
1298
+ setCamposAvaliacao([])
1299
+ valoresAvaliacaoRef.current = {}
1300
+ setAvaliacaoFormVersion((prev) => prev + 1)
1301
+ setAvaliacaoPendente(false)
1302
+ setResultadoAvaliacaoHtml('')
1303
+ setStatus(resp.status)
1304
+ }
1305
+ }
1306
+
1307
  async function onUploadClick(arquivo = null) {
1308
  const arquivoUpload = arquivo || uploadedFile
1309
  if (!arquivoUpload || !sessionId) return
 
1315
  const uploadEhDai = nomeArquivo.endsWith('.dai')
1316
  setTipoFonteDados(uploadEhDai ? 'dai' : 'tabular')
1317
  const resp = await api.uploadElaboracaoFile(sessionId, arquivoUpload)
1318
+ aplicarRespostaCarregamento(resp, uploadEhDai ? 'dai' : 'tabular')
1319
+ })
1320
+ }
1321
+
1322
+ async function onCarregarModeloRepositorio() {
1323
+ if (!sessionId || !repoModeloSelecionado) return
1324
+ await withBusy(async () => {
1325
+ setMapaGerado(false)
1326
+ setMapaHtml('')
1327
+ setGeoAuto200(true)
1328
+ setTipoFonteDados('dai')
1329
+ const resp = await api.elaboracaoRepositorioCarregar(sessionId, repoModeloSelecionado)
1330
+ aplicarRespostaCarregamento(resp, 'dai')
1331
+ setUploadedFile(null)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1332
  })
1333
  }
1334
 
 
2060
  <div className="section1-groups">
2061
  <div className="subpanel section1-group">
2062
  <h4>Carregar modelo</h4>
2063
+ <div className="row upload-repo-row">
2064
+ <label>Modelo do repositório</label>
2065
+ <select
2066
+ value={repoModeloSelecionado}
2067
+ onChange={(e) => setRepoModeloSelecionado(e.target.value)}
2068
+ disabled={loading || repoModelosLoading || repoModelos.length === 0}
2069
+ >
2070
+ <option value="">
2071
+ {repoModelosLoading ? 'Carregando lista...' : repoModelos.length > 0 ? 'Selecione um modelo' : 'Nenhum modelo disponível'}
2072
+ </option>
2073
+ {repoModelos.map((item) => (
2074
+ <option key={`repo-elab-${item.id}`} value={item.id}>
2075
+ {item.nome_modelo || item.arquivo}
2076
+ </option>
2077
+ ))}
2078
+ </select>
2079
+ <div className="row compact upload-repo-actions">
2080
+ <button type="button" onClick={onCarregarModeloRepositorio} disabled={loading || repoModelosLoading || !repoModeloSelecionado}>
2081
+ Carregar do repositório
2082
+ </button>
2083
+ <button type="button" onClick={() => void carregarModelosRepositorio()} disabled={loading || repoModelosLoading}>
2084
+ Atualizar lista
2085
+ </button>
2086
+ </div>
2087
+ {repoFonteModelos ? <div className="section1-empty-hint">{repoFonteModelos}</div> : null}
2088
+ </div>
2089
  <div
2090
  className={`upload-dropzone${uploadDragOver ? ' is-dragover' : ''}`}
2091
  onDragOver={onUploadDropZoneDragOver}
frontend/src/components/VisualizacaoTab.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useRef, useState } from 'react'
2
  import { api, downloadBlob } from '../api'
3
  import DataTable from './DataTable'
4
  import LoadingOverlay from './LoadingOverlay'
@@ -27,6 +27,10 @@ export default function VisualizacaoTab({ sessionId }) {
27
 
28
  const [uploadedFile, setUploadedFile] = useState(null)
29
  const [uploadDragOver, setUploadDragOver] = useState(false)
 
 
 
 
30
 
31
  const [dados, setDados] = useState(null)
32
  const [estatisticas, setEstatisticas] = useState(null)
@@ -129,6 +133,73 @@ export default function VisualizacaoTab({ sessionId }) {
129
  }
130
  }
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  async function onUploadModel(arquivo = null) {
133
  const arquivoUpload = arquivo || uploadedFile
134
  if (!sessionId || !arquivoUpload) return
@@ -143,6 +214,20 @@ export default function VisualizacaoTab({ sessionId }) {
143
  })
144
  }
145
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  function onUploadInputChange(event) {
147
  const input = event.target
148
  const file = input.files?.[0] ?? null
@@ -277,6 +362,32 @@ export default function VisualizacaoTab({ sessionId }) {
277
  return (
278
  <div className="tab-content">
279
  <SectionBlock step="1" title="Carregar Modelo .dai" subtitle="Carregue o arquivo e o conteúdo será exibido automaticamente.">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  <div
281
  className={`upload-dropzone${uploadDragOver ? ' is-dragover' : ''}`}
282
  onDragOver={onUploadDropZoneDragOver}
 
1
+ import React, { useEffect, useRef, useState } from 'react'
2
  import { api, downloadBlob } from '../api'
3
  import DataTable from './DataTable'
4
  import LoadingOverlay from './LoadingOverlay'
 
27
 
28
  const [uploadedFile, setUploadedFile] = useState(null)
29
  const [uploadDragOver, setUploadDragOver] = useState(false)
30
+ const [repoModelos, setRepoModelos] = useState([])
31
+ const [repoModeloSelecionado, setRepoModeloSelecionado] = useState('')
32
+ const [repoModelosLoading, setRepoModelosLoading] = useState(false)
33
+ const [repoFonteModelos, setRepoFonteModelos] = useState('')
34
 
35
  const [dados, setDados] = useState(null)
36
  const [estatisticas, setEstatisticas] = useState(null)
 
133
  }
134
  }
135
 
136
+ function formatarFonteRepositorio(fonte) {
137
+ if (!fonte || typeof fonte !== 'object') return ''
138
+ const provider = String(fonte.provider || '').toLowerCase()
139
+ if (provider === 'hf_dataset') {
140
+ const repo = String(fonte.repo_id || '').trim()
141
+ const revision = String(fonte.revision || '').trim()
142
+ const degradado = Boolean(fonte.degraded)
143
+ const sufixo = degradado ? ' (modo contingência)' : ''
144
+ return `Fonte: HF Dataset${repo ? ` (${repo})` : ''}${revision ? ` | revisão ${revision.slice(0, 8)}` : ''}${sufixo}`
145
+ }
146
+ return 'Fonte: pasta local'
147
+ }
148
+
149
+ function aplicarRespostaModelosRepositorio(resp) {
150
+ const modelos = Array.isArray(resp?.modelos) ? resp.modelos : []
151
+ setRepoModelos(modelos)
152
+ setRepoFonteModelos(formatarFonteRepositorio(resp?.fonte || null))
153
+ setRepoModeloSelecionado((prev) => {
154
+ const atual = String(prev || '')
155
+ if (atual && modelos.some((item) => String(item.id) === atual)) return atual
156
+ return String(modelos[0]?.id || '')
157
+ })
158
+ }
159
+
160
+ async function carregarModelosRepositorio() {
161
+ setRepoModelosLoading(true)
162
+ try {
163
+ const resp = await api.visualizacaoRepositorioModelos()
164
+ aplicarRespostaModelosRepositorio(resp)
165
+ } catch (err) {
166
+ setError(err.message || 'Falha ao carregar modelos do repositório.')
167
+ setRepoModelos([])
168
+ setRepoModeloSelecionado('')
169
+ setRepoFonteModelos('')
170
+ } finally {
171
+ setRepoModelosLoading(false)
172
+ }
173
+ }
174
+
175
+ useEffect(() => {
176
+ let ativo = true
177
+ if (!sessionId) return () => {
178
+ ativo = false
179
+ }
180
+
181
+ setRepoModelosLoading(true)
182
+ api.visualizacaoRepositorioModelos()
183
+ .then((resp) => {
184
+ if (!ativo) return
185
+ aplicarRespostaModelosRepositorio(resp)
186
+ })
187
+ .catch(() => {
188
+ if (!ativo) return
189
+ setRepoModelos([])
190
+ setRepoModeloSelecionado('')
191
+ setRepoFonteModelos('')
192
+ })
193
+ .finally(() => {
194
+ if (!ativo) return
195
+ setRepoModelosLoading(false)
196
+ })
197
+
198
+ return () => {
199
+ ativo = false
200
+ }
201
+ }, [sessionId])
202
+
203
  async function onUploadModel(arquivo = null) {
204
  const arquivoUpload = arquivo || uploadedFile
205
  if (!sessionId || !arquivoUpload) return
 
214
  })
215
  }
216
 
217
+ async function onCarregarModeloRepositorio() {
218
+ if (!sessionId || !repoModeloSelecionado) return
219
+ await withBusy(async () => {
220
+ resetConteudoVisualizacao()
221
+ const uploadResp = await api.visualizacaoRepositorioCarregar(sessionId, repoModeloSelecionado)
222
+ setStatus(uploadResp.status || '')
223
+ setBadgeHtml(uploadResp.badge_html || '')
224
+
225
+ const exibirResp = await api.exibirVisualizacao(sessionId)
226
+ applyExibicaoResponse(exibirResp)
227
+ setUploadedFile(null)
228
+ })
229
+ }
230
+
231
  function onUploadInputChange(event) {
232
  const input = event.target
233
  const file = input.files?.[0] ?? null
 
362
  return (
363
  <div className="tab-content">
364
  <SectionBlock step="1" title="Carregar Modelo .dai" subtitle="Carregue o arquivo e o conteúdo será exibido automaticamente.">
365
+ <div className="row upload-repo-row">
366
+ <label>Modelo do repositório</label>
367
+ <select
368
+ value={repoModeloSelecionado}
369
+ onChange={(e) => setRepoModeloSelecionado(e.target.value)}
370
+ disabled={loading || repoModelosLoading || repoModelos.length === 0}
371
+ >
372
+ <option value="">
373
+ {repoModelosLoading ? 'Carregando lista...' : repoModelos.length > 0 ? 'Selecione um modelo' : 'Nenhum modelo disponível'}
374
+ </option>
375
+ {repoModelos.map((item) => (
376
+ <option key={`repo-viz-${item.id}`} value={item.id}>
377
+ {item.nome_modelo || item.arquivo}
378
+ </option>
379
+ ))}
380
+ </select>
381
+ <div className="row compact upload-repo-actions">
382
+ <button type="button" onClick={onCarregarModeloRepositorio} disabled={loading || repoModelosLoading || !repoModeloSelecionado}>
383
+ Carregar do repositório
384
+ </button>
385
+ <button type="button" onClick={() => void carregarModelosRepositorio()} disabled={loading || repoModelosLoading}>
386
+ Atualizar lista
387
+ </button>
388
+ </div>
389
+ {repoFonteModelos ? <div className="section1-empty-hint">{repoFonteModelos}</div> : null}
390
+ </div>
391
  <div
392
  className={`upload-dropzone${uploadDragOver ? ' is-dragover' : ''}`}
393
  onDragOver={onUploadDropZoneDragOver}
frontend/src/styles.css CHANGED
@@ -1345,6 +1345,14 @@ button:disabled {
1345
  transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
1346
  }
1347
 
 
 
 
 
 
 
 
 
1348
  .upload-dropzone.is-dragover {
1349
  border-color: #3b7fb8;
1350
  background: linear-gradient(180deg, #f3f9ff 0%, #ebf4ff 100%);
 
1345
  transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
1346
  }
1347
 
1348
+ .upload-repo-row {
1349
+ margin-bottom: 10px;
1350
+ }
1351
+
1352
+ .upload-repo-actions {
1353
+ margin-top: 2px;
1354
+ }
1355
+
1356
  .upload-dropzone.is-dragover {
1357
  border-color: #3b7fb8;
1358
  background: linear-gradient(180deg, #f3f9ff 0%, #ebf4ff 100%);