Update app.py
Browse files
app.py
CHANGED
|
@@ -2,7 +2,8 @@ import os
|
|
| 2 |
import io
|
| 3 |
import json
|
| 4 |
import uuid
|
| 5 |
-
|
|
|
|
| 6 |
from datetime import datetime, timezone
|
| 7 |
|
| 8 |
import numpy as np
|
|
@@ -24,10 +25,13 @@ HF_MERGED_REPO = os.getenv("HF_MERGED_REPO")
|
|
| 24 |
HF_EMB_REPO = os.getenv("HF_EMB_REPO")
|
| 25 |
MODEL_NAME = os.getenv("MODEL_NAME")
|
| 26 |
|
| 27 |
-
# Репозиторий для заявок
|
| 28 |
HF_REQUESTS_REPO = os.getenv("HF_REQUESTS_REPO", "PlanetExpress2125/PostDocRequests")
|
| 29 |
HF_REQUESTS_REPO_TYPE = os.getenv("HF_REQUESTS_REPO_TYPE", "dataset") # dataset | model
|
| 30 |
-
HF_WRITE_TOKEN = os.getenv("HF_WRITE_TOKEN") #
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
SLIDER_MIN_YEAR = 2005 # нижняя граница диапазона лет
|
| 33 |
|
|
@@ -41,6 +45,7 @@ COLUMN_LABELS_RU_EXCEL = {
|
|
| 41 |
"protection_year": "Год",
|
| 42 |
"registration_number": "Регистрационный номер",
|
| 43 |
"vak_link": "Ссылка ВАК",
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
DISPLAY_COLUMNS_ALL = [
|
|
@@ -144,11 +149,11 @@ def _keyify(label: str) -> str:
|
|
| 144 |
def save_request_to_hub(payload: dict) -> str:
|
| 145 |
"""
|
| 146 |
Сохраняем каждую заявку отдельным JSON-файлом в:
|
| 147 |
-
|
| 148 |
|
| 149 |
Требуется токен с правами write:
|
| 150 |
- HF_WRITE_TOKEN (рекомендуется)
|
| 151 |
-
- если не задан, будет использован HF_TOKEN
|
| 152 |
"""
|
| 153 |
token = HF_WRITE_TOKEN or HF_TOKEN
|
| 154 |
if not token:
|
|
@@ -172,6 +177,109 @@ def save_request_to_hub(payload: dict) -> str:
|
|
| 172 |
return path_in_repo
|
| 173 |
|
| 174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
# ==========================
|
| 176 |
# ЗАГРУЗКА ДАННЫХ И МОДЕЛИ
|
| 177 |
# ==========================
|
|
@@ -355,7 +463,7 @@ def build_result_df(results):
|
|
| 355 |
if not rows:
|
| 356 |
return pd.DataFrame(columns=DISPLAY_COLUMNS_ALL + ["vak_link"])
|
| 357 |
|
| 358 |
-
df_res = pd.DataFrame(rows)
|
| 359 |
return df_res[DISPLAY_COLUMNS_ALL + ["vak_link"]]
|
| 360 |
|
| 361 |
|
|
@@ -371,8 +479,14 @@ def run_search(
|
|
| 371 |
results = search_core(query, top_k, mask=mask)
|
| 372 |
df_raw = build_result_df(results)
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
# UI: скрываем №, reg номер, тип
|
| 375 |
-
df_ui = df_raw.copy()
|
| 376 |
df_ui = df_ui.rename(
|
| 377 |
columns={
|
| 378 |
"score": "Сходство",
|
|
@@ -381,12 +495,15 @@ def run_search(
|
|
| 381 |
"author_org_short": "Организация",
|
| 382 |
"protection_year": "Год",
|
| 383 |
"vak_link": "ВАК",
|
|
|
|
| 384 |
}
|
| 385 |
)
|
| 386 |
df_ui = df_ui.drop(columns=["№", "registration_number", "dissertation_type"])
|
| 387 |
|
| 388 |
-
# Excel: полный набор
|
| 389 |
-
|
|
|
|
|
|
|
| 390 |
output = io.BytesIO()
|
| 391 |
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
|
| 392 |
df_excel_ru.to_excel(writer, index=False)
|
|
@@ -398,7 +515,7 @@ def run_search(
|
|
| 398 |
def format_selected_list_for_ui(df_ui_show: pd.DataFrame, selected_idx: List[int]) -> str:
|
| 399 |
lines = []
|
| 400 |
for i in selected_idx:
|
| 401 |
-
r = df_ui_show.
|
| 402 |
fio = str(r.get("ФИО", "")).strip()
|
| 403 |
title = str(r.get("Название диссертации", "")).strip()
|
| 404 |
year = r.get("Год", "")
|
|
@@ -501,15 +618,16 @@ excel_saved = st.session_state.last_excel
|
|
| 501 |
if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
| 502 |
st.success(f"Найдено записей: {len(df_ui_saved)}")
|
| 503 |
|
| 504 |
-
|
| 505 |
-
|
|
|
|
| 506 |
|
| 507 |
# Таблица: сортировка по клику по заголовку + мультивыбор строк (чекбоксы)
|
| 508 |
event = st.dataframe(
|
| 509 |
df_ui_show,
|
| 510 |
use_container_width=True,
|
| 511 |
hide_index=True,
|
| 512 |
-
column_order=["Сходство", "ФИО", "Название диссертации", "Организация", "Год", "ВАК"],
|
| 513 |
column_config={
|
| 514 |
"Сходство": st.column_config.NumberColumn("Сходство", format="%.4f", width="small"),
|
| 515 |
"ФИО": st.column_config.TextColumn("ФИО", width="medium"),
|
|
@@ -517,15 +635,21 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
|
| 517 |
"Организация": st.column_config.TextColumn("Организация", width="medium"),
|
| 518 |
"Год": st.column_config.NumberColumn("Год", width="small"),
|
| 519 |
"ВАК": st.column_config.LinkColumn("ВАК", display_text="открыть", width="small"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
},
|
| 521 |
on_select="rerun",
|
| 522 |
selection_mode="multi-row",
|
| 523 |
)
|
| 524 |
|
| 525 |
-
# сохраняем выбранные
|
| 526 |
sel = getattr(event, "selection", None)
|
| 527 |
if sel is not None and sel.rows is not None:
|
| 528 |
-
# sel.rows — индексы строк (не обязательно 0..N после сортировки в UI)
|
| 529 |
st.session_state.selected_rows = list(sel.rows)
|
| 530 |
|
| 531 |
if excel_saved is not None:
|
|
@@ -536,7 +660,7 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
|
| 536 |
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 537 |
)
|
| 538 |
|
| 539 |
-
# ======= ЗАПРОС РАСШИРЕННОЙ ИНФОРМАЦИИ (ЗА
|
| 540 |
st.markdown("---")
|
| 541 |
st.subheader("Запрос расширенной информации")
|
| 542 |
|
|
@@ -554,8 +678,11 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
|
| 554 |
|
| 555 |
comment = st.text_area(
|
| 556 |
"Комментарий",
|
| 557 |
-
height=
|
| 558 |
-
placeholder=
|
|
|
|
|
|
|
|
|
|
| 559 |
)
|
| 560 |
|
| 561 |
send_request = st.form_submit_button("📨 Отправить запрос", type="primary", use_container_width=True)
|
|
@@ -566,10 +693,10 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
|
| 566 |
elif len(selected_rows) == 0:
|
| 567 |
st.warning("Выберите хотя бы одну диссертацию в таблице (чекбоксами слева).")
|
| 568 |
else:
|
| 569 |
-
#
|
| 570 |
items = []
|
| 571 |
-
for
|
| 572 |
-
raw = df_raw_saved.
|
| 573 |
items.append(
|
| 574 |
{
|
| 575 |
"author_fio": raw.get("fio"),
|
|
@@ -579,6 +706,7 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
|
| 579 |
"vak_link": raw.get("vak_link"),
|
| 580 |
"registration_number": raw.get("registration_number"),
|
| 581 |
"score": raw.get("score"),
|
|
|
|
| 582 |
}
|
| 583 |
)
|
| 584 |
|
|
@@ -595,12 +723,7 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
|
| 595 |
|
| 596 |
try:
|
| 597 |
path = save_request_to_hub(payload)
|
| 598 |
-
st.success(f"Запрос сохран
|
| 599 |
-
|
| 600 |
-
# При желании можно показать, что ушло (на время отладки)
|
| 601 |
-
with st.expander("Показать содержимое заявки (для проверки)", expanded=False):
|
| 602 |
-
st.json(payload)
|
| 603 |
-
|
| 604 |
except Exception as e:
|
| 605 |
st.error(
|
| 606 |
"Не удалось сохранить запрос в репозиторий PostDocRequests.\n\n"
|
|
|
|
| 2 |
import io
|
| 3 |
import json
|
| 4 |
import uuid
|
| 5 |
+
import re
|
| 6 |
+
from typing import Optional, Tuple, List, Dict, Any
|
| 7 |
from datetime import datetime, timezone
|
| 8 |
|
| 9 |
import numpy as np
|
|
|
|
| 25 |
HF_EMB_REPO = os.getenv("HF_EMB_REPO")
|
| 26 |
MODEL_NAME = os.getenv("MODEL_NAME")
|
| 27 |
|
| 28 |
+
# Репозиторий для заявок
|
| 29 |
HF_REQUESTS_REPO = os.getenv("HF_REQUESTS_REPO", "PlanetExpress2125/PostDocRequests")
|
| 30 |
HF_REQUESTS_REPO_TYPE = os.getenv("HF_REQUESTS_REPO_TYPE", "dataset") # dataset | model
|
| 31 |
+
HF_WRITE_TOKEN = os.getenv("HF_WRITE_TOKEN") # write token (рекомендуется)
|
| 32 |
+
|
| 33 |
+
# OpenAlex index dataset
|
| 34 |
+
OA_INDEX_REPO = os.getenv("OA_INDEX_REPO", "yogl/diss_authors_with_oa_index")
|
| 35 |
|
| 36 |
SLIDER_MIN_YEAR = 2005 # нижняя граница диапазона лет
|
| 37 |
|
|
|
|
| 45 |
"protection_year": "Год",
|
| 46 |
"registration_number": "Регистрационный номер",
|
| 47 |
"vak_link": "Ссылка ВАК",
|
| 48 |
+
"openalex_url": "OpenAlex",
|
| 49 |
}
|
| 50 |
|
| 51 |
DISPLAY_COLUMNS_ALL = [
|
|
|
|
| 149 |
def save_request_to_hub(payload: dict) -> str:
|
| 150 |
"""
|
| 151 |
Сохраняем каждую заявку отдельным JSON-файлом в:
|
| 152 |
+
<HF_REQUESTS_REPO>/requests/<timestamp>_<uuid>.json
|
| 153 |
|
| 154 |
Требуется токен с правами write:
|
| 155 |
- HF_WRITE_TOKEN (рекомендуется)
|
| 156 |
+
- если не задан, будет использован HF_TOKEN (если он write)
|
| 157 |
"""
|
| 158 |
token = HF_WRITE_TOKEN or HF_TOKEN
|
| 159 |
if not token:
|
|
|
|
| 177 |
return path_in_repo
|
| 178 |
|
| 179 |
|
| 180 |
+
# ==========================
|
| 181 |
+
# OPENALEX INDEX
|
| 182 |
+
# ==========================
|
| 183 |
+
|
| 184 |
+
def _normalize_regnum(x: Any) -> Optional[str]:
|
| 185 |
+
if x is None:
|
| 186 |
+
return None
|
| 187 |
+
s = str(x).strip()
|
| 188 |
+
if not s or s.lower() == "nan":
|
| 189 |
+
return None
|
| 190 |
+
return s
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def _iter_registration_numbers(value: Any) -> List[str]:
|
| 194 |
+
"""
|
| 195 |
+
registration_numbers может быть:
|
| 196 |
+
- list/tuple
|
| 197 |
+
- строкой (один номер или несколько через , ; пробел)
|
| 198 |
+
- строкой вида "['123','456']"
|
| 199 |
+
"""
|
| 200 |
+
if value is None:
|
| 201 |
+
return []
|
| 202 |
+
if isinstance(value, (list, tuple, np.ndarray)):
|
| 203 |
+
out = []
|
| 204 |
+
for v in value:
|
| 205 |
+
r = _normalize_regnum(v)
|
| 206 |
+
if r:
|
| 207 |
+
out.append(r)
|
| 208 |
+
return out
|
| 209 |
+
|
| 210 |
+
s = str(value).strip()
|
| 211 |
+
if not s or s.lower() == "nan":
|
| 212 |
+
return []
|
| 213 |
+
|
| 214 |
+
# попытка распарсить список
|
| 215 |
+
if (s.startswith("[") and s.endswith("]")) or (s.startswith("(") and s.endswith(")")):
|
| 216 |
+
try:
|
| 217 |
+
parsed = json.loads(s)
|
| 218 |
+
if isinstance(parsed, list):
|
| 219 |
+
return [r for r in (_normalize_regnum(v) for v in parsed) if r]
|
| 220 |
+
except Exception:
|
| 221 |
+
pass
|
| 222 |
+
|
| 223 |
+
# fallback: split
|
| 224 |
+
parts = re.split(r"[,\s;]+", s)
|
| 225 |
+
return [r for r in (_normalize_regnum(p) for p in parts) if r]
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def _normalize_oa_url(value: Any) -> str:
|
| 229 |
+
if value is None:
|
| 230 |
+
return ""
|
| 231 |
+
s = str(value).strip()
|
| 232 |
+
if not s or s.lower() == "nan":
|
| 233 |
+
return ""
|
| 234 |
+
# если уже URL
|
| 235 |
+
if s.startswith("http://") or s.startswith("https://"):
|
| 236 |
+
# нормализуем домен/слеши
|
| 237 |
+
m = re.search(r"(A\d+)", s)
|
| 238 |
+
if m:
|
| 239 |
+
return f"https://openalex.org/{m.group(1)}"
|
| 240 |
+
return s
|
| 241 |
+
# если только ID
|
| 242 |
+
m = re.match(r"^A\d+$", s)
|
| 243 |
+
if m:
|
| 244 |
+
return f"https://openalex.org/{s}"
|
| 245 |
+
# если вдруг мусор — попробуем вытащить A...
|
| 246 |
+
m = re.search(r"(A\d+)", s)
|
| 247 |
+
if m:
|
| 248 |
+
return f"https://openalex.org/{m.group(1)}"
|
| 249 |
+
return ""
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
@st.cache_data(show_spinner="Загрузка индекса OpenAlex...")
|
| 253 |
+
def load_openalex_map() -> Dict[str, str]:
|
| 254 |
+
"""
|
| 255 |
+
Строим маппинг: registration_number -> openalex_url
|
| 256 |
+
из yogl/diss_authors_with_oa_index (поле registration_numbers + oa_author_id).
|
| 257 |
+
"""
|
| 258 |
+
try:
|
| 259 |
+
ds = load_dataset(OA_INDEX_REPO, split="train")
|
| 260 |
+
df = ds.to_pandas()
|
| 261 |
+
except Exception:
|
| 262 |
+
return {}
|
| 263 |
+
|
| 264 |
+
if "registration_numbers" not in df.columns or "oa_author_id" not in df.columns:
|
| 265 |
+
return {}
|
| 266 |
+
|
| 267 |
+
mapping: Dict[str, str] = {}
|
| 268 |
+
for _, row in df.iterrows():
|
| 269 |
+
regs = row.get("registration_numbers", None)
|
| 270 |
+
oa = row.get("oa_author_id", None)
|
| 271 |
+
url = _normalize_oa_url(oa)
|
| 272 |
+
if not url:
|
| 273 |
+
continue
|
| 274 |
+
for reg in _iter_registration_numbers(regs):
|
| 275 |
+
if reg and reg not in mapping:
|
| 276 |
+
mapping[reg] = url
|
| 277 |
+
return mapping
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
OA_MAP = load_openalex_map()
|
| 281 |
+
|
| 282 |
+
|
| 283 |
# ==========================
|
| 284 |
# ЗАГРУЗКА ДАННЫХ И МОДЕЛИ
|
| 285 |
# ==========================
|
|
|
|
| 463 |
if not rows:
|
| 464 |
return pd.DataFrame(columns=DISPLAY_COLUMNS_ALL + ["vak_link"])
|
| 465 |
|
| 466 |
+
df_res = pd.DataFrame(rows).reset_index(drop=True)
|
| 467 |
return df_res[DISPLAY_COLUMNS_ALL + ["vak_link"]]
|
| 468 |
|
| 469 |
|
|
|
|
| 479 |
results = search_core(query, top_k, mask=mask)
|
| 480 |
df_raw = build_result_df(results)
|
| 481 |
|
| 482 |
+
# OpenAlex URL по registration_number
|
| 483 |
+
if not df_raw.empty and OA_MAP:
|
| 484 |
+
df_raw["openalex_url"] = df_raw["registration_number"].astype(str).map(OA_MAP).fillna("")
|
| 485 |
+
else:
|
| 486 |
+
df_raw["openalex_url"] = ""
|
| 487 |
+
|
| 488 |
# UI: скрываем №, reg номер, тип
|
| 489 |
+
df_ui = df_raw.copy().reset_index(drop=True)
|
| 490 |
df_ui = df_ui.rename(
|
| 491 |
columns={
|
| 492 |
"score": "Сходство",
|
|
|
|
| 495 |
"author_org_short": "Организация",
|
| 496 |
"protection_year": "Год",
|
| 497 |
"vak_link": "ВАК",
|
| 498 |
+
"openalex_url": "OpenAlex",
|
| 499 |
}
|
| 500 |
)
|
| 501 |
df_ui = df_ui.drop(columns=["№", "registration_number", "dissertation_type"])
|
| 502 |
|
| 503 |
+
# Excel: полный набор + OpenAlex
|
| 504 |
+
df_excel = df_raw.copy()
|
| 505 |
+
df_excel_ru = df_excel.rename(columns=COLUMN_LABELS_RU_EXCEL)
|
| 506 |
+
|
| 507 |
output = io.BytesIO()
|
| 508 |
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
|
| 509 |
df_excel_ru.to_excel(writer, index=False)
|
|
|
|
| 515 |
def format_selected_list_for_ui(df_ui_show: pd.DataFrame, selected_idx: List[int]) -> str:
|
| 516 |
lines = []
|
| 517 |
for i in selected_idx:
|
| 518 |
+
r = df_ui_show.iloc[i]
|
| 519 |
fio = str(r.get("ФИО", "")).strip()
|
| 520 |
title = str(r.get("Название диссертации", "")).strip()
|
| 521 |
year = r.get("Год", "")
|
|
|
|
| 618 |
if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
| 619 |
st.success(f"Найдено записей: {len(df_ui_saved)}")
|
| 620 |
|
| 621 |
+
df_ui_show = df_ui_saved[
|
| 622 |
+
["Сходство", "ФИО", "Название диссертации", "Организация", "Год", "ВАК", "OpenAlex"]
|
| 623 |
+
].copy()
|
| 624 |
|
| 625 |
# Таблица: сортировка по клику по заголовку + мультивыбор строк (чекбоксы)
|
| 626 |
event = st.dataframe(
|
| 627 |
df_ui_show,
|
| 628 |
use_container_width=True,
|
| 629 |
hide_index=True,
|
| 630 |
+
column_order=["Сходство", "ФИО", "Название диссертации", "Организация", "Год", "ВАК", "OpenAlex"],
|
| 631 |
column_config={
|
| 632 |
"Сходство": st.column_config.NumberColumn("Сходство", format="%.4f", width="small"),
|
| 633 |
"ФИО": st.column_config.TextColumn("ФИО", width="medium"),
|
|
|
|
| 635 |
"Организация": st.column_config.TextColumn("Организация", width="medium"),
|
| 636 |
"Год": st.column_config.NumberColumn("Год", width="small"),
|
| 637 |
"ВАК": st.column_config.LinkColumn("ВАК", display_text="открыть", width="small"),
|
| 638 |
+
# Отображаем ID (A123...) из URL через regex capture group
|
| 639 |
+
"OpenAlex": st.column_config.LinkColumn(
|
| 640 |
+
"OpenAlex",
|
| 641 |
+
display_text=r"https://openalex\.org/(A\d+)",
|
| 642 |
+
width="small",
|
| 643 |
+
help="Профиль автора в OpenAlex (если найден)",
|
| 644 |
+
),
|
| 645 |
},
|
| 646 |
on_select="rerun",
|
| 647 |
selection_mode="multi-row",
|
| 648 |
)
|
| 649 |
|
| 650 |
+
# сохраняем выбранные строки (позиции 0..N-1 исходного df_ui_show)
|
| 651 |
sel = getattr(event, "selection", None)
|
| 652 |
if sel is not None and sel.rows is not None:
|
|
|
|
| 653 |
st.session_state.selected_rows = list(sel.rows)
|
| 654 |
|
| 655 |
if excel_saved is not None:
|
|
|
|
| 660 |
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 661 |
)
|
| 662 |
|
| 663 |
+
# ======= ЗАПРОС РАСШИРЕННОЙ ИНФОРМАЦИИ (ЗАПИСЬ В PostDocRequests) =======
|
| 664 |
st.markdown("---")
|
| 665 |
st.subheader("Запрос расширенной информации")
|
| 666 |
|
|
|
|
| 678 |
|
| 679 |
comment = st.text_area(
|
| 680 |
"Комментарий",
|
| 681 |
+
height=140,
|
| 682 |
+
placeholder=(
|
| 683 |
+
"Какую дополнительную информацию по авторам диссертаций вы хотите получить?\n"
|
| 684 |
+
"Какие замечания/пожелания по функционалу системы?"
|
| 685 |
+
),
|
| 686 |
)
|
| 687 |
|
| 688 |
send_request = st.form_submit_button("📨 Отправить запрос", type="primary", use_container_width=True)
|
|
|
|
| 693 |
elif len(selected_rows) == 0:
|
| 694 |
st.warning("Выберите хотя бы одну диссертацию в таблице (чекбоксами слева).")
|
| 695 |
else:
|
| 696 |
+
# items по выбранным строкам
|
| 697 |
items = []
|
| 698 |
+
for i in selected_rows:
|
| 699 |
+
raw = df_raw_saved.iloc[i].to_dict()
|
| 700 |
items.append(
|
| 701 |
{
|
| 702 |
"author_fio": raw.get("fio"),
|
|
|
|
| 706 |
"vak_link": raw.get("vak_link"),
|
| 707 |
"registration_number": raw.get("registration_number"),
|
| 708 |
"score": raw.get("score"),
|
| 709 |
+
"openalex_url": raw.get("openalex_url", ""),
|
| 710 |
}
|
| 711 |
)
|
| 712 |
|
|
|
|
| 723 |
|
| 724 |
try:
|
| 725 |
path = save_request_to_hub(payload)
|
| 726 |
+
st.success(f"Запрос {path} сохранен")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 727 |
except Exception as e:
|
| 728 |
st.error(
|
| 729 |
"Не удалось сохранить запрос в репозиторий PostDocRequests.\n\n"
|