sitayeb commited on
Commit
ef047c0
ยท
verified ยท
1 Parent(s): a40680d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +189 -51
app.py CHANGED
@@ -1,12 +1,13 @@
1
  # ================================================================
2
- # ๐Ÿ”ฌ Scientific Paper Discovery Bot โ€” v7.2
3
- # FIX: CrossRef garbage dates (2048, 2116, 2117...) now rejected
4
  # ================================================================
5
  import os, re, time, json, pickle, threading
6
  import requests
7
  import xml.etree.ElementTree as ET
8
  from datetime import datetime, timedelta
9
  from collections import Counter
 
10
 
11
  import numpy as np
12
  import faiss
@@ -19,6 +20,14 @@ from sentence_transformers import SentenceTransformer
19
  from groq import Groq
20
  from gtts import gTTS
21
  from langdetect import detect, DetectorFactory
 
 
 
 
 
 
 
 
22
  DetectorFactory.seed = 0
23
 
24
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
@@ -35,7 +44,7 @@ ACTIVE_PAPERS = []
35
  FAISS_INDEX = None
36
  AUTO_RUNNING = False
37
  AUTO_LOG = []
38
- CURRENT_YEAR = datetime.now().year # 2026
39
 
40
  PERSIST_DIR = "/tmp"
41
  FAVORITES_PATH = f"{PERSIST_DIR}/favorites.pkl"
@@ -122,31 +131,24 @@ def build_table(papers_list):
122
  return rows, choices
123
 
124
  def s2_headers():
125
- h = {"User-Agent": "ScientificPaperBot/7.2"}
126
  if S2_API_KEY: h["x-api-key"] = S2_API_KEY
127
  return h
128
 
129
  def cr_headers():
130
- return {"User-Agent": "ScientificPaperBot/7.2 (mailto:researcher@example.com)"}
131
 
132
  # ================================================================
133
- # โœ… FIXED: CrossRef date parser โ€” rejects garbage years
134
  # ================================================================
135
  def parse_crossref_date(item: dict) -> str:
136
- """
137
- CrossRef sometimes stores volume/issue/page numbers in date-parts
138
- causing years like 2048, 2103, 2116, 2117 etc.
139
- We try 5 date fields in priority order and reject any year
140
- outside the range [1900, CURRENT_YEAR+1].
141
- """
142
  for field in ["issued", "published", "published-print", "published-online", "created"]:
143
  dp = (item.get(field) or {}).get("date-parts", [[]])
144
  if not dp or not dp[0]: continue
145
  pts = dp[0]
146
  try:
147
  year = int(pts[0])
148
- if not (1900 <= year <= CURRENT_YEAR + 1):
149
- continue # โ† reject garbage year
150
  month = max(1, min(12, int(pts[1]) if len(pts) >= 2 else 1))
151
  day = max(1, min(31, int(pts[2]) if len(pts) >= 3 else 1))
152
  return f"{year:04d}-{month:02d}-{day:02d}"
@@ -190,6 +192,86 @@ def export_favorites_csv():
190
 
191
  def gr_export_fav(): return export_favorites_csv()
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  # ================================================================
194
  # SOURCE 1 โ€” arXiv
195
  # ================================================================
@@ -235,14 +317,14 @@ def fetch_arxiv_papers(query, category, max_results=20, days_back=365):
235
  return papers
236
 
237
  # ================================================================
238
- # SOURCE 2 โ€” CrossRef (with fixed date parser + title filter)
239
  # ================================================================
240
  def fetch_crossref_papers(query, category_label="", max_results=20, days_back=365):
241
  subject = CROSSREF_SUBJECTS.get(category_label, "")
242
  full_query = f"{query} {subject}".strip() if subject else query
243
  params = {
244
  "query": full_query,
245
- "rows": min(max_results * 3, 200), # fetch more, filter bad dates after
246
  "sort": "relevance",
247
  "select": "title,author,abstract,published,published-print,published-online,issued,created,DOI,is-referenced-by-count,link,subject",
248
  }
@@ -261,34 +343,27 @@ def fetch_crossref_papers(query, category_label="", max_results=20, days_back=36
261
  papers, seen_ids = [], set()
262
  for item in items:
263
  if len(papers) >= max_results: break
264
-
265
  title_list = item.get("title", [])
266
  if not title_list: continue
267
  title = title_list[0].strip()
268
- if not title or title.lower().startswith("title pending"): continue # skip junk
269
-
270
- # โœ… Use fixed date parser
271
  pub = parse_crossref_date(item)
272
- if pub == "N/A": continue # skip items with no valid date at all
273
-
274
  cit = int(item.get("is-referenced-by-count", 0) or 0)
275
  authors = [f"{a.get('given','').strip()} {a.get('family','').strip()}".strip()
276
  for a in item.get("author",[])[:6]]
277
  authors = [a for a in authors if a.strip()]
278
  if not authors: authors = ["Unknown"]
279
-
280
  abstract = re.sub(r"<[^>]+>","", item.get("abstract","No abstract.")).strip()[:1200]
281
  doi = item.get("DOI","")
282
  url = f"https://doi.org/{doi}" if doi else "#"
283
  pid = doi or re.sub(r"\W","",title)[:40]
284
  if pid in seen_ids: continue
285
  seen_ids.add(pid)
286
-
287
  pdf_url = next((l.get("URL","") for l in item.get("link",[])
288
  if "pdf" in l.get("content-type","").lower()), "")
289
  try: recent = datetime.strptime(pub[:10],"%Y-%m-%d") >= cutoff
290
  except: recent = False
291
-
292
  papers.append({
293
  "id": pid, "title": title, "authors": authors,
294
  "abstract": abstract, "published": pub[:10],
@@ -296,10 +371,42 @@ def fetch_crossref_papers(query, category_label="", max_results=20, days_back=36
296
  "citations": cit, "url": url, "pdf_url": pdf_url,
297
  "recent": recent, "source": "CrossRef",
298
  })
299
-
300
  papers.sort(key=lambda x: x["citations"], reverse=True)
301
  return papers
302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  # ================================================================
304
  # CITATION ENGINE โ€” 3-layer
305
  # ================================================================
@@ -310,7 +417,6 @@ def enrich_citations(papers):
310
  for p in papers:
311
  if p.get("citations") is None: p["citations"] = 0
312
  return papers
313
-
314
  id_map, batch_ids = {}, []
315
  for p in arxiv_papers:
316
  clean = re.sub(r"v\d+$","", p["id"].split("/")[-1].strip())
@@ -639,7 +745,6 @@ def gr_fetch(query, category_label, max_results, days_back, source_choice,
639
  progress(1.0)
640
  return md, upd, upd, upd, upd, f"โœ… {len(papers)} | ๐Ÿ†• {recent} | ๐Ÿ“Š {tot_cit:,}"
641
 
642
-
643
  def gr_filter_papers(year_from, year_to, cit_min, cit_max, sort_by):
644
  global ACTIVE_PAPERS
645
  if not PAPERS: return "โš ๏ธ ุงุฌู„ุจ ุงู„ุฃูˆุฑุงู‚ ุฃูˆู„ุงู‹.", gr.update(), "โš ๏ธ"
@@ -667,7 +772,6 @@ def gr_filter_papers(year_from, year_to, cit_min, cit_max, sort_by):
667
  f"๐Ÿ“Š ุงู‚ุชุจุงุณุงุช {cit_min}โ€“{cit_max} &nbsp;|&nbsp; ู…ุฌู…ูˆุน {tot:,}\n\n---\n\n{tbl}")
668
  return md, gr.update(choices=choices, value=choices[0] if choices else None), f"๐Ÿ”ฝ {len(filtered)}/{len(PAPERS)}"
669
 
670
-
671
  def gr_search_fetched(query):
672
  if not query or not query.strip(): return "โš ๏ธ ุฃุฏุฎู„ ูƒู„ู…ุฉ ุจุญุซ."
673
  if not PAPERS: return "โš ๏ธ ุงุฌู„ุจ ุงู„ุฃูˆุฑุงู‚ ุฃูˆู„ุงู‹."
@@ -766,14 +870,15 @@ h1{text-align:center}
766
  .status-bar{font-size:.85rem;color:#94a3b8;padding:2px 0}
767
  .legend{font-size:.8rem;color:#cbd5e1;background:#1e293b;border-radius:8px;padding:6px 14px;margin-bottom:6px}
768
  .filter-box{background:#1e293b;border-radius:10px;padding:12px 16px;margin-top:8px}
 
769
  """
770
 
771
  with gr.Blocks(
772
  theme=gr.themes.Soft(primary_hue="blue", secondary_hue="purple"),
773
- title="๐Ÿ”ฌ Paper Discovery v7.2", css=CSS
774
  ) as demo:
775
 
776
- gr.Markdown("# ๐Ÿ”ฌ Scientific Paper Discovery v7.2\n**arXiv ยท CrossRef ยท Llama-3.3-70B ยท FAISS**")
777
  gr.Markdown(
778
  "๐Ÿ“Š **ุงู„ุงู‚ุชุจุงุณุงุช:** &nbsp; ๐Ÿฅ‡ โ‰ฅ 1,000 &nbsp;|&nbsp; "
779
  "๐Ÿ† โ‰ฅ 100 &nbsp;|&nbsp; โญ โ‰ฅ 10 &nbsp;|&nbsp; ๐Ÿ“„ < 10 &nbsp;|&nbsp; ยท = 0",
@@ -781,6 +886,7 @@ with gr.Blocks(
781
  status_bar = gr.Markdown("_ู„ู… ูŠุชู… ุฌู„ุจ ุฃูˆุฑุงู‚ ุจุนุฏ_", elem_classes="status-bar")
782
 
783
  with gr.Tabs():
 
784
  with gr.Tab("๐Ÿ” ุงู„ุจุญุซ / Search"):
785
  with gr.Row():
786
  with gr.Column(scale=3):
@@ -804,24 +910,46 @@ with gr.Blocks(
804
  with gr.Row():
805
  f_sort = gr.Dropdown(choices=SORT_CHOICES, value=SORT_CHOICES[2], label="๐Ÿ”ƒ ุงู„ุชุฑุชูŠุจ", scale=3)
806
  btn_filter = gr.Button("โœ… ุชุทุจูŠู‚", variant="primary", scale=1)
807
- gr.Markdown("---\n### ๐Ÿ” ุจุญุซ ุฏู„ุงู„ูŠ")
808
  with gr.Row():
809
  search_in_box = gr.Textbox(label="๐Ÿ” ุงุจุญุซ", placeholder="ARIMA, transformer...", scale=5)
810
  btn_search_in = gr.Button("ุจุญุซ ๐Ÿ”", scale=1)
811
  search_in_out = gr.Markdown()
812
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
813
  with gr.Tab("๐Ÿ“– ุงู„ุดุฑุญ / Explain"):
814
  with gr.Row():
815
  paper_sel2 = gr.Dropdown(label="๐Ÿ“„ ุงุฎุชุฑ ุงู„ูˆุฑู‚ุฉ", choices=[], interactive=True, scale=4)
816
  lang_exp = gr.Radio(LANG_CHOICES, value=LANG_CHOICES[0], label="๐ŸŒ ุงู„ู„ุบุฉ", scale=1)
817
  with gr.Row():
818
- btn_explain = gr.Button("๐Ÿ“– ุงุดุฑุญ", variant="primary")
819
- btn_fav = gr.Button("โญ ุญูุธ")
820
- btn_audio = gr.Button("๐Ÿ”Š ุงุณุชู…ุน")
 
821
  fav_status = gr.Markdown()
 
822
  explanation_out = gr.Markdown("_ุงุฌู„ุจ ุงู„ุฃูˆุฑุงู‚ ูˆุงุฎุชุฑ ูˆุฑู‚ุฉ._")
823
  audio_out = gr.Audio(label="๐Ÿ”Š", type="filepath")
 
824
 
 
825
  with gr.Tab("โš–๏ธ ุงู„ู…ู‚ุงุฑู†ุฉ / Compare"):
826
  with gr.Row():
827
  cmp_a = gr.Dropdown(label="๐Ÿ“„ ุงู„ุฃูˆู„ู‰", choices=[], interactive=True)
@@ -830,29 +958,34 @@ with gr.Blocks(
830
  btn_compare = gr.Button("โš–๏ธ ู‚ุงุฑู† ุงู„ุขู†", variant="primary")
831
  compare_out = gr.Markdown("_ุงุฎุชุฑ ูˆุฑู‚ุชูŠู†._")
832
 
 
833
  with gr.Tab("๐ŸŒ ู†ุธุฑุฉ ุนุงู…ุฉ"):
834
  with gr.Row():
835
  lang_ov = gr.Radio(LANG_CHOICES, value=LANG_CHOICES[0], label="๐ŸŒ ุงู„ู„ุบุฉ", scale=1)
836
  btn_overview = gr.Button("๐Ÿค– ุชูˆู„ูŠุฏ ุงู„ุชู‚ุฑูŠุฑ", variant="primary", scale=3)
837
  overview_out = gr.Markdown("_ุงุฌู„ุจ ุงู„ุฃูˆุฑุงู‚ ุฃูˆู„ุงู‹._")
838
 
 
839
  with gr.Tab("๐Ÿ“Š ุงู„ุงุชุฌุงู‡ุงุช / Trends"):
840
  btn_trends = gr.Button("๐Ÿ“Š ุชุญู„ูŠู„ ุงู„ุงุชุฌุงู‡ุงุช", variant="primary", size="lg")
841
  trend_chart = gr.Image(label="๐Ÿ“Š ู„ูˆุญุฉ ุงู„ุงุชุฌุงู‡ุงุช", type="filepath")
842
  trend_stats = gr.Markdown("_ุงุฌู„ุจ ุงู„ุฃูˆุฑุงู‚ ุฃูˆู„ุงู‹._")
843
 
 
844
  with gr.Tab("๐Ÿ“š ุงู„ู…ุฑุงุฌุน"):
845
  bib_style = gr.Radio(["APA","IEEE","Chicago","BibTeX"], value="APA", label="๐Ÿ“ ุงู„ู†ู…ุท")
846
  btn_bib = gr.Button("๐Ÿ“š ุชูˆู„ูŠุฏ ุงู„ู…ุฑุงุฌุน", variant="primary")
847
  bib_out = gr.Markdown()
848
  bib_file = gr.File(label="๐Ÿ“ฅ ุชุญู…ูŠู„")
849
 
 
850
  with gr.Tab("โญ ุงู„ู…ูุถู„ุฉ"):
851
  btn_show_fav = gr.Button("๐Ÿ“‹ ุนุฑุถ ุงู„ู…ูุถู„ุฉ")
852
  favs_md = gr.Markdown("_ุงุถุบุท ุนุฑุถ._")
853
  btn_export_fav = gr.Button("๐Ÿ“ฅ ุชุตุฏูŠุฑ CSV", variant="secondary")
854
  fav_csv_file = gr.File(label="๐Ÿ“„ CSV")
855
 
 
856
  with gr.Tab("๐Ÿ”” ุชุญุฏูŠุซ ุชู„ู‚ุงุฆูŠ"):
857
  with gr.Row():
858
  auto_q = gr.Textbox(label="๐Ÿ”Ž ุงู„ู…ูˆุถูˆุน", value="economic forecasting", scale=3)
@@ -865,6 +998,7 @@ with gr.Blocks(
865
  auto_status = gr.Markdown()
866
  auto_log_md = gr.Markdown("_ู„ุง ูŠูˆุฌุฏ ุณุฌู„._")
867
 
 
868
  with gr.Tab("๐Ÿ’ฌ ู…ุญุงุฏุซุฉ / Chat"):
869
  chatbot_ui = gr.Chatbot(label="ู…ุณุงุนุฏ ุงู„ุฃุจุญุงุซ", height=480, bubble_full_width=False)
870
  with gr.Row():
@@ -872,26 +1006,22 @@ with gr.Blocks(
872
  btn_send = gr.Button("ุฅุฑุณุงู„ โœ‰๏ธ", variant="primary", scale=1)
873
  btn_clear = gr.Button("๐Ÿ—‘๏ธ ู…ุณุญ", size="sm")
874
 
 
875
  with gr.Tab("โ„น๏ธ ุญูˆู„"):
876
  gr.Markdown("""
877
- ## ๐Ÿ”ฌ Scientific Paper Discovery โ€” v7.2
 
 
 
 
 
 
878
 
879
- ### โœ… ุฅุตู„ุงุญุงุช v7.2
880
  | ุงู„ู…ุดูƒู„ุฉ | ุงู„ุฅุตู„ุงุญ |
881
  |---|---|
882
- | ุชูˆุงุฑูŠุฎ CrossRef ู…ุฒูŠูุฉ (2048ุŒ 2116ุŒ 2117...) | `parse_crossref_date()` ุชุฑูุถ ุฃูŠ ุณู†ุฉ ุฎุงุฑุฌ `[1900, 2027]` |
883
- | `published-print` ู‚ุฏ ูŠุญูˆูŠ ุฑู‚ู… ุงู„ู…ุฌู„ุฏ | ุฃูˆู„ูˆูŠุฉ ุงู„ุญู‚ูˆู„: `issued โ†’ published โ†’ published-print โ†’ published-online โ†’ created` |
884
- | ุนู†ุงูˆูŠู† "Title Pending" | ูู„ุชุฑ `title.lower().startswith("title pending")` ูŠุญุฐูู‡ุง |
885
- | ุฅุนุงุฏุฉ ุชุดุบูŠู„ ุนู†ุฏ ุงู„ูู„ุชุฑ | Filter โ†’ 3 outputs ูู‚ุท (ู„ุง cascade) โœ… |
886
-
887
- ### ๐Ÿ”ง ุฏุงู„ุฉ parse_crossref_date
888
- ```python
889
- for field in ["issued", "published", "published-print", "published-online", "created"]:
890
- year = int(pts[0])
891
- if not (1900 <= year <= CURRENT_YEAR + 1):
892
- continue # โ† reject 2048, 2116, 2117...
893
- return f"{year:04d}-{month:02d}-{day:02d}"
894
- ```
895
  """)
896
 
897
  # โ”€โ”€ WIRING โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -902,16 +1032,24 @@ for field in ["issued", "published", "published-print", "published-online", "cre
902
  outputs=FETCH_OUT)
903
  btn_filter.click(gr_filter_papers,
904
  inputs=[f_year_from, f_year_to, f_cit_min, f_cit_max, f_sort],
905
- outputs=[papers_table_md, paper_selector, status_bar]) # 3 only โœ…
906
  paper_selector.change(lambda x: [gr.update(value=x)]*3,
907
  inputs=[paper_selector], outputs=[paper_sel2, cmp_a, cmp_b])
908
 
909
  btn_search_in.click(gr_search_fetched, inputs=[search_in_box], outputs=[search_in_out])
910
  search_in_box.submit(gr_search_fetched, inputs=[search_in_box], outputs=[search_in_out])
911
 
 
 
 
 
 
912
  btn_explain.click(gr_explain, inputs=[paper_sel2, lang_exp], outputs=[explanation_out])
913
  btn_fav.click(gr_save_fav, inputs=[paper_sel2], outputs=[fav_status])
914
  btn_audio.click(gr_audio, inputs=[explanation_out, lang_exp], outputs=[audio_out])
 
 
 
915
 
916
  btn_compare.click(gr_compare, inputs=[cmp_a, cmp_b, lang_cmp], outputs=[compare_out])
917
  btn_overview.click(gr_overview, inputs=[t_query, lang_ov], outputs=[overview_out])
 
1
  # ================================================================
2
+ # ๐Ÿ”ฌ Scientific Paper Discovery Bot โ€” v7.3
3
+ # NEW: PDF Export for Explanation + ๐Ÿ”Ž Global Paper Search Tab
4
  # ================================================================
5
  import os, re, time, json, pickle, threading
6
  import requests
7
  import xml.etree.ElementTree as ET
8
  from datetime import datetime, timedelta
9
  from collections import Counter
10
+ from io import BytesIO
11
 
12
  import numpy as np
13
  import faiss
 
20
  from groq import Groq
21
  from gtts import gTTS
22
  from langdetect import detect, DetectorFactory
23
+ from reportlab.lib.pagesizes import A4
24
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
25
+ from reportlab.lib.units import cm
26
+ from reportlab.lib import colors
27
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
28
+ from reportlab.pdfbase import pdfmetrics
29
+ from reportlab.pdfbase.ttfonts import TTFont
30
+
31
  DetectorFactory.seed = 0
32
 
33
  GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "")
 
44
  FAISS_INDEX = None
45
  AUTO_RUNNING = False
46
  AUTO_LOG = []
47
+ CURRENT_YEAR = datetime.now().year
48
 
49
  PERSIST_DIR = "/tmp"
50
  FAVORITES_PATH = f"{PERSIST_DIR}/favorites.pkl"
 
131
  return rows, choices
132
 
133
  def s2_headers():
134
+ h = {"User-Agent": "ScientificPaperBot/7.3"}
135
  if S2_API_KEY: h["x-api-key"] = S2_API_KEY
136
  return h
137
 
138
  def cr_headers():
139
+ return {"User-Agent": "ScientificPaperBot/7.3 (mailto:researcher@example.com)"}
140
 
141
  # ================================================================
142
+ # โœ… CrossRef date parser โ€” rejects garbage years
143
  # ================================================================
144
  def parse_crossref_date(item: dict) -> str:
 
 
 
 
 
 
145
  for field in ["issued", "published", "published-print", "published-online", "created"]:
146
  dp = (item.get(field) or {}).get("date-parts", [[]])
147
  if not dp or not dp[0]: continue
148
  pts = dp[0]
149
  try:
150
  year = int(pts[0])
151
+ if not (1900 <= year <= CURRENT_YEAR + 1): continue
 
152
  month = max(1, min(12, int(pts[1]) if len(pts) >= 2 else 1))
153
  day = max(1, min(31, int(pts[2]) if len(pts) >= 3 else 1))
154
  return f"{year:04d}-{month:02d}-{day:02d}"
 
192
 
193
  def gr_export_fav(): return export_favorites_csv()
194
 
195
+ # ================================================================
196
+ # โœ… NEW: PDF EXPORT
197
+ # ================================================================
198
+ def export_explanation_pdf(explanation_text, paper_title="paper"):
199
+ if not explanation_text or len(explanation_text) < 30:
200
+ return None
201
+ safe_title = re.sub(r"[^\w\s-]", "", paper_title)[:50].strip().replace(" ", "_")
202
+ path = f"{PERSIST_DIR}/explanation_{safe_title}.pdf"
203
+
204
+ doc = SimpleDocTemplate(path, pagesize=A4,
205
+ rightMargin=2*cm, leftMargin=2*cm,
206
+ topMargin=2*cm, bottomMargin=2*cm)
207
+ styles = getSampleStyleSheet()
208
+
209
+ title_style = ParagraphStyle("Title2", parent=styles["Title"],
210
+ fontSize=14, textColor=colors.HexColor("#1e3a5f"),
211
+ spaceAfter=12)
212
+ h2_style = ParagraphStyle("H2", parent=styles["Heading2"],
213
+ fontSize=11, textColor=colors.HexColor("#2563eb"),
214
+ spaceBefore=14, spaceAfter=6)
215
+ body_style = ParagraphStyle("Body", parent=styles["Normal"],
216
+ fontSize=10, leading=16, spaceAfter=8)
217
+ meta_style = ParagraphStyle("Meta", parent=styles["Normal"],
218
+ fontSize=9, textColor=colors.HexColor("#64748b"),
219
+ spaceAfter=10)
220
+
221
+ story = []
222
+ lines = explanation_text.split("\n")
223
+ for line in lines:
224
+ line = line.strip()
225
+ if not line: story.append(Spacer(1, 6)); continue
226
+
227
+ # strip markdown artifacts for PDF
228
+ clean = re.sub(r"\*\*(.+?)\*\*", r"\1", line)
229
+ clean = re.sub(r"\*(.+?)\*", r"\1", clean)
230
+ clean = re.sub(r"`(.+?)`", r"\1", clean)
231
+ clean = re.sub(r"^#{1,6}\s*", "", clean)
232
+ clean = re.sub(r"[๐ŸŽฏโ“๐Ÿ”ง๐Ÿ“Š๐ŸŒŸ๐Ÿ”—๐Ÿ“„๐Ÿ‘ฅ๐Ÿ“…๐Ÿ“ก๐Ÿค–#*_~]", "", clean).strip()
233
+
234
+ if line.startswith("## ") or line.startswith("# "):
235
+ story.append(HRFlowable(width="100%", thickness=0.5,
236
+ color=colors.HexColor("#e2e8f0"), spaceAfter=4))
237
+ story.append(Paragraph(clean, h2_style))
238
+ elif line.startswith("**") and line.endswith("**"):
239
+ story.append(Paragraph(f"<b>{clean}</b>", body_style))
240
+ elif line.startswith(">"):
241
+ quote = line.lstrip(">").strip()
242
+ quote = re.sub(r"[๐ŸŽฏโ“๐Ÿ”ง๐Ÿ“Š๐ŸŒŸ๐Ÿ”—๐Ÿ“„๐Ÿ‘ฅ๐Ÿ“…๐Ÿ“ก๐Ÿค–#*_~]", "", quote).strip()
243
+ q_style = ParagraphStyle("Quote", parent=styles["Normal"],
244
+ fontSize=9, leftIndent=20, rightIndent=10,
245
+ textColor=colors.HexColor("#475569"),
246
+ borderPadding=(4,4,4,8),
247
+ leading=14, spaceAfter=8)
248
+ story.append(Paragraph(quote, q_style))
249
+ else:
250
+ if clean:
251
+ story.append(Paragraph(clean, body_style))
252
+
253
+ # Footer
254
+ story.append(Spacer(1, 20))
255
+ story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor("#e2e8f0")))
256
+ story.append(Paragraph(
257
+ f"Generated by ๐Ÿ”ฌ Scientific Paper Discovery v7.3 โ€” {datetime.now().strftime('%Y-%m-%d %H:%M')}",
258
+ meta_style))
259
+
260
+ try:
261
+ doc.build(story)
262
+ return path
263
+ except Exception as e:
264
+ print(f"PDF error: {e}")
265
+ return None
266
+
267
+ def gr_export_pdf(explanation_text, choice):
268
+ if not explanation_text or len(explanation_text) < 50:
269
+ return None, "โš ๏ธ ุงุดุฑุญ ุงู„ูˆุฑู‚ุฉ ุฃูˆู„ุงู‹ ุซู… ุตุฏู‘ุฑ PDF."
270
+ title = choice.split(". ", 1)[-1] if choice else "paper"
271
+ path = export_explanation_pdf(explanation_text, title)
272
+ if path: return path, "โœ… ุชู… ุฅู†ุดุงุก PDF!"
273
+ return None, "โŒ ูุดู„ ุฅู†ุดุงุก PDF."
274
+
275
  # ================================================================
276
  # SOURCE 1 โ€” arXiv
277
  # ================================================================
 
317
  return papers
318
 
319
  # ================================================================
320
+ # SOURCE 2 โ€” CrossRef
321
  # ================================================================
322
  def fetch_crossref_papers(query, category_label="", max_results=20, days_back=365):
323
  subject = CROSSREF_SUBJECTS.get(category_label, "")
324
  full_query = f"{query} {subject}".strip() if subject else query
325
  params = {
326
  "query": full_query,
327
+ "rows": min(max_results * 3, 200),
328
  "sort": "relevance",
329
  "select": "title,author,abstract,published,published-print,published-online,issued,created,DOI,is-referenced-by-count,link,subject",
330
  }
 
343
  papers, seen_ids = [], set()
344
  for item in items:
345
  if len(papers) >= max_results: break
 
346
  title_list = item.get("title", [])
347
  if not title_list: continue
348
  title = title_list[0].strip()
349
+ if not title or title.lower().startswith("title pending"): continue
 
 
350
  pub = parse_crossref_date(item)
351
+ if pub == "N/A": continue
 
352
  cit = int(item.get("is-referenced-by-count", 0) or 0)
353
  authors = [f"{a.get('given','').strip()} {a.get('family','').strip()}".strip()
354
  for a in item.get("author",[])[:6]]
355
  authors = [a for a in authors if a.strip()]
356
  if not authors: authors = ["Unknown"]
 
357
  abstract = re.sub(r"<[^>]+>","", item.get("abstract","No abstract.")).strip()[:1200]
358
  doi = item.get("DOI","")
359
  url = f"https://doi.org/{doi}" if doi else "#"
360
  pid = doi or re.sub(r"\W","",title)[:40]
361
  if pid in seen_ids: continue
362
  seen_ids.add(pid)
 
363
  pdf_url = next((l.get("URL","") for l in item.get("link",[])
364
  if "pdf" in l.get("content-type","").lower()), "")
365
  try: recent = datetime.strptime(pub[:10],"%Y-%m-%d") >= cutoff
366
  except: recent = False
 
367
  papers.append({
368
  "id": pid, "title": title, "authors": authors,
369
  "abstract": abstract, "published": pub[:10],
 
371
  "citations": cit, "url": url, "pdf_url": pdf_url,
372
  "recent": recent, "source": "CrossRef",
373
  })
 
374
  papers.sort(key=lambda x: x["citations"], reverse=True)
375
  return papers
376
 
377
+ # ================================================================
378
+ # โœ… NEW: GLOBAL PAPER SEARCH (independent of local index)
379
+ # ================================================================
380
+ def global_paper_search(query, source_choice, max_results=10):
381
+ if not query or not query.strip():
382
+ return "โš ๏ธ ุฃุฏุฎู„ ุนู†ูˆุงู† ุฃูˆ ูƒู„ู…ุงุช ู…ูุชุงุญูŠุฉ ู„ู„ุจุญุซ."
383
+ papers = []
384
+ q = query.strip()
385
+ if source_choice in ("arXiv", "ูƒู„ุงู‡ู…ุง / Both"):
386
+ papers += fetch_arxiv_papers(q, "", int(max_results), 3650)
387
+ if source_choice in ("CrossRef", "ูƒู„ุงู‡ู…ุง / Both"):
388
+ papers += fetch_crossref_papers(q, "", int(max_results), 3650)
389
+ if not papers:
390
+ return f"โŒ ู„ุง ู†ุชุงุฆุฌ ู„ู€ `{q}`. ุฌุฑุจ ูƒู„ู…ุงุช ู…ุฎุชู„ูุฉ."
391
+
392
+ seen, unique = set(), []
393
+ for p in papers:
394
+ key = re.sub(r"\W","",p["title"].lower())[:60]
395
+ if key not in seen: seen.add(key); unique.append(p)
396
+
397
+ md = f"## ๐Ÿ”Ž ู†ุชุงุฆุฌ ุงู„ุจุญุซ ุงู„ุนุงู„ู…ูŠ โ€” `{q}` โ€” **{len(unique)}** ูˆุฑู‚ุฉ\n\n---\n\n"
398
+ for i, p in enumerate(unique, 1):
399
+ cit = f" | {cit_badge(p.get('citations'))}" if p.get("citations") else ""
400
+ cats = " ยท ".join(p.get("categories",[])[:2])
401
+ md += (f"### {i}. {p['title']}\n\n"
402
+ f"๐Ÿ‘ฅ {', '.join(p['authors'][:3])} | ๐Ÿ“… {p['published']}{cit} | "
403
+ f"๐Ÿ“ก {p.get('source','')} | ๐Ÿท๏ธ {cats}\n\n"
404
+ f"> {p['abstract'][:400]}...\n\n"
405
+ f"๐Ÿ”— [View]({p['url']})"
406
+ +(f" ๐Ÿ“ฅ [PDF]({p['pdf_url']})" if p.get("pdf_url") else "")
407
+ +"\n\n---\n\n")
408
+ return md
409
+
410
  # ================================================================
411
  # CITATION ENGINE โ€” 3-layer
412
  # ================================================================
 
417
  for p in papers:
418
  if p.get("citations") is None: p["citations"] = 0
419
  return papers
 
420
  id_map, batch_ids = {}, []
421
  for p in arxiv_papers:
422
  clean = re.sub(r"v\d+$","", p["id"].split("/")[-1].strip())
 
745
  progress(1.0)
746
  return md, upd, upd, upd, upd, f"โœ… {len(papers)} | ๐Ÿ†• {recent} | ๐Ÿ“Š {tot_cit:,}"
747
 
 
748
  def gr_filter_papers(year_from, year_to, cit_min, cit_max, sort_by):
749
  global ACTIVE_PAPERS
750
  if not PAPERS: return "โš ๏ธ ุงุฌู„ุจ ุงู„ุฃูˆุฑุงู‚ ุฃูˆู„ุงู‹.", gr.update(), "โš ๏ธ"
 
772
  f"๐Ÿ“Š ุงู‚ุชุจุงุณุงุช {cit_min}โ€“{cit_max} &nbsp;|&nbsp; ู…ุฌู…ูˆุน {tot:,}\n\n---\n\n{tbl}")
773
  return md, gr.update(choices=choices, value=choices[0] if choices else None), f"๐Ÿ”ฝ {len(filtered)}/{len(PAPERS)}"
774
 
 
775
  def gr_search_fetched(query):
776
  if not query or not query.strip(): return "โš ๏ธ ุฃุฏุฎู„ ูƒู„ู…ุฉ ุจุญุซ."
777
  if not PAPERS: return "โš ๏ธ ุงุฌู„ุจ ุงู„ุฃูˆุฑุงู‚ ุฃูˆู„ุงู‹."
 
870
  .status-bar{font-size:.85rem;color:#94a3b8;padding:2px 0}
871
  .legend{font-size:.8rem;color:#cbd5e1;background:#1e293b;border-radius:8px;padding:6px 14px;margin-bottom:6px}
872
  .filter-box{background:#1e293b;border-radius:10px;padding:12px 16px;margin-top:8px}
873
+ .global-search-box{background:#1e293b;border-radius:10px;padding:14px 18px;margin-bottom:10px;border:1px solid #334155}
874
  """
875
 
876
  with gr.Blocks(
877
  theme=gr.themes.Soft(primary_hue="blue", secondary_hue="purple"),
878
+ title="๐Ÿ”ฌ Paper Discovery v7.3", css=CSS
879
  ) as demo:
880
 
881
+ gr.Markdown("# ๐Ÿ”ฌ Scientific Paper Discovery v7.3\n**arXiv ยท CrossRef ยท Llama-3.3-70B ยท FAISS**")
882
  gr.Markdown(
883
  "๐Ÿ“Š **ุงู„ุงู‚ุชุจุงุณุงุช:** &nbsp; ๐Ÿฅ‡ โ‰ฅ 1,000 &nbsp;|&nbsp; "
884
  "๐Ÿ† โ‰ฅ 100 &nbsp;|&nbsp; โญ โ‰ฅ 10 &nbsp;|&nbsp; ๐Ÿ“„ < 10 &nbsp;|&nbsp; ยท = 0",
 
886
  status_bar = gr.Markdown("_ู„ู… ูŠุชู… ุฌู„ุจ ุฃูˆุฑุงู‚ ุจุนุฏ_", elem_classes="status-bar")
887
 
888
  with gr.Tabs():
889
+ # โ”€โ”€ TAB 1: SEARCH โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
890
  with gr.Tab("๐Ÿ” ุงู„ุจุญุซ / Search"):
891
  with gr.Row():
892
  with gr.Column(scale=3):
 
910
  with gr.Row():
911
  f_sort = gr.Dropdown(choices=SORT_CHOICES, value=SORT_CHOICES[2], label="๐Ÿ”ƒ ุงู„ุชุฑุชูŠุจ", scale=3)
912
  btn_filter = gr.Button("โœ… ุชุทุจูŠู‚", variant="primary", scale=1)
913
+ gr.Markdown("---\n### ๐Ÿ” ุจุญุซ ุฏู„ุงู„ูŠ (ููŠ ุงู„ุฃูˆุฑุงู‚ ุงู„ู…ุญู…ู„ุฉ)")
914
  with gr.Row():
915
  search_in_box = gr.Textbox(label="๐Ÿ” ุงุจุญุซ", placeholder="ARIMA, transformer...", scale=5)
916
  btn_search_in = gr.Button("ุจุญุซ ๐Ÿ”", scale=1)
917
  search_in_out = gr.Markdown()
918
 
919
+ # โ”€โ”€ TAB 2: GLOBAL SEARCH โœ… NEW โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
920
+ with gr.Tab("๐ŸŒ ุจุญุซ ุนุงู„ู…ูŠ / Global Search"):
921
+ gr.Markdown("### ๐ŸŒ ุงุจุญุซ ุนู† ุฃูŠ ูˆุฑู‚ุฉ ู…ุจุงุดุฑุฉ โ€” ุจุฏูˆู† ุฌู„ุจ ู…ุณุจู‚")
922
+ with gr.Group(elem_classes="global-search-box"):
923
+ with gr.Row():
924
+ gs_query = gr.Textbox(
925
+ label="๐Ÿ”Ž ุงุจุญุซ ุจุงู„ุนู†ูˆุงู† ุฃูˆ ุงู„ูƒู„ู…ุงุช ุงู„ู…ูุชุงุญูŠุฉ",
926
+ placeholder="ู…ุซุงู„: ARIMA forecasting inflation Algeria ...",
927
+ scale=4)
928
+ gs_source = gr.Radio(
929
+ label="๐Ÿ“ก ุงู„ู…ุตุฏุฑ",
930
+ choices=["arXiv","CrossRef","ูƒู„ุงู‡ู…ุง / Both"],
931
+ value="ูƒู„ุงู‡ู…ุง / Both", scale=2)
932
+ gs_max = gr.Slider(5, 30, value=10, step=5, label="๐Ÿ“Š ุนุฏุฏ ุงู„ู†ุชุงุฆุฌ", scale=1)
933
+ btn_gs = gr.Button("๐Ÿ”Ž ุจุญุซ ุนุงู„ู…ูŠ", variant="primary", size="lg")
934
+ gs_out = gr.Markdown("_ุฃุฏุฎู„ ู…ูˆุถูˆุนุงู‹ ู„ู„ุจุญุซ..._")
935
+
936
+ # โ”€โ”€ TAB 3: EXPLAIN โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
937
  with gr.Tab("๐Ÿ“– ุงู„ุดุฑุญ / Explain"):
938
  with gr.Row():
939
  paper_sel2 = gr.Dropdown(label="๐Ÿ“„ ุงุฎุชุฑ ุงู„ูˆุฑู‚ุฉ", choices=[], interactive=True, scale=4)
940
  lang_exp = gr.Radio(LANG_CHOICES, value=LANG_CHOICES[0], label="๐ŸŒ ุงู„ู„ุบุฉ", scale=1)
941
  with gr.Row():
942
+ btn_explain = gr.Button("๐Ÿ“– ุงุดุฑุญ", variant="primary")
943
+ btn_fav = gr.Button("โญ ุญูุธ")
944
+ btn_audio = gr.Button("๐Ÿ”Š ุงุณุชู…ุน")
945
+ btn_export_pdf = gr.Button("๐Ÿ“„ ุชุตุฏูŠุฑ PDF", variant="secondary") # โœ… NEW
946
  fav_status = gr.Markdown()
947
+ pdf_status = gr.Markdown() # โœ… NEW
948
  explanation_out = gr.Markdown("_ุงุฌู„ุจ ุงู„ุฃูˆุฑุงู‚ ูˆุงุฎุชุฑ ูˆุฑู‚ุฉ._")
949
  audio_out = gr.Audio(label="๐Ÿ”Š", type="filepath")
950
+ pdf_out = gr.File(label="๐Ÿ“„ ุชุญู…ูŠู„ PDF") # โœ… NEW
951
 
952
+ # โ”€โ”€ TAB 4: COMPARE โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
953
  with gr.Tab("โš–๏ธ ุงู„ู…ู‚ุงุฑู†ุฉ / Compare"):
954
  with gr.Row():
955
  cmp_a = gr.Dropdown(label="๐Ÿ“„ ุงู„ุฃูˆู„ู‰", choices=[], interactive=True)
 
958
  btn_compare = gr.Button("โš–๏ธ ู‚ุงุฑู† ุงู„ุขู†", variant="primary")
959
  compare_out = gr.Markdown("_ุงุฎุชุฑ ูˆุฑู‚ุชูŠู†._")
960
 
961
+ # โ”€โ”€ TAB 5: OVERVIEW โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
962
  with gr.Tab("๐ŸŒ ู†ุธุฑุฉ ุนุงู…ุฉ"):
963
  with gr.Row():
964
  lang_ov = gr.Radio(LANG_CHOICES, value=LANG_CHOICES[0], label="๐ŸŒ ุงู„ู„ุบุฉ", scale=1)
965
  btn_overview = gr.Button("๐Ÿค– ุชูˆู„ูŠุฏ ุงู„ุชู‚ุฑูŠุฑ", variant="primary", scale=3)
966
  overview_out = gr.Markdown("_ุงุฌู„ุจ ุงู„ุฃูˆุฑุงู‚ ุฃูˆู„ุงู‹._")
967
 
968
+ # โ”€โ”€ TAB 6: TRENDS โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
969
  with gr.Tab("๐Ÿ“Š ุงู„ุงุชุฌุงู‡ุงุช / Trends"):
970
  btn_trends = gr.Button("๐Ÿ“Š ุชุญู„ูŠู„ ุงู„ุงุชุฌุงู‡ุงุช", variant="primary", size="lg")
971
  trend_chart = gr.Image(label="๐Ÿ“Š ู„ูˆุญุฉ ุงู„ุงุชุฌุงู‡ุงุช", type="filepath")
972
  trend_stats = gr.Markdown("_ุงุฌู„ุจ ุงู„ุฃูˆุฑุงู‚ ุฃูˆู„ุงู‹._")
973
 
974
+ # โ”€โ”€ TAB 7: BIBLIOGRAPHY โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
975
  with gr.Tab("๐Ÿ“š ุงู„ู…ุฑุงุฌุน"):
976
  bib_style = gr.Radio(["APA","IEEE","Chicago","BibTeX"], value="APA", label="๐Ÿ“ ุงู„ู†ู…ุท")
977
  btn_bib = gr.Button("๐Ÿ“š ุชูˆู„ูŠุฏ ุงู„ู…ุฑุงุฌุน", variant="primary")
978
  bib_out = gr.Markdown()
979
  bib_file = gr.File(label="๐Ÿ“ฅ ุชุญู…ูŠู„")
980
 
981
+ # โ”€โ”€ TAB 8: FAVORITES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
982
  with gr.Tab("โญ ุงู„ู…ูุถู„ุฉ"):
983
  btn_show_fav = gr.Button("๐Ÿ“‹ ุนุฑุถ ุงู„ู…ูุถู„ุฉ")
984
  favs_md = gr.Markdown("_ุงุถุบุท ุนุฑุถ._")
985
  btn_export_fav = gr.Button("๐Ÿ“ฅ ุชุตุฏูŠุฑ CSV", variant="secondary")
986
  fav_csv_file = gr.File(label="๐Ÿ“„ CSV")
987
 
988
+ # โ”€โ”€ TAB 9: AUTO-FETCH โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€๏ฟฝ๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
989
  with gr.Tab("๐Ÿ”” ุชุญุฏูŠุซ ุชู„ู‚ุงุฆูŠ"):
990
  with gr.Row():
991
  auto_q = gr.Textbox(label="๐Ÿ”Ž ุงู„ู…ูˆุถูˆุน", value="economic forecasting", scale=3)
 
998
  auto_status = gr.Markdown()
999
  auto_log_md = gr.Markdown("_ู„ุง ูŠูˆุฌุฏ ุณุฌู„._")
1000
 
1001
+ # โ”€โ”€ TAB 10: CHAT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1002
  with gr.Tab("๐Ÿ’ฌ ู…ุญุงุฏุซุฉ / Chat"):
1003
  chatbot_ui = gr.Chatbot(label="ู…ุณุงุนุฏ ุงู„ุฃุจุญุงุซ", height=480, bubble_full_width=False)
1004
  with gr.Row():
 
1006
  btn_send = gr.Button("ุฅุฑุณุงู„ โœ‰๏ธ", variant="primary", scale=1)
1007
  btn_clear = gr.Button("๐Ÿ—‘๏ธ ู…ุณุญ", size="sm")
1008
 
1009
+ # โ”€โ”€ TAB 11: ABOUT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1010
  with gr.Tab("โ„น๏ธ ุญูˆู„"):
1011
  gr.Markdown("""
1012
+ ## ๐Ÿ”ฌ Scientific Paper Discovery โ€” v7.3
1013
+
1014
+ ### โœ… ุฌุฏูŠุฏ ููŠ v7.3
1015
+ | ุงู„ู…ูŠุฒุฉ | ุงู„ุชูุงุตูŠู„ |
1016
+ |---|---|
1017
+ | ๐Ÿ“„ ุชุตุฏูŠุฑ PDF | ุชุญูˆูŠู„ ุงู„ุดุฑุญ ุงู„ูƒุงู…ู„ ุฅู„ู‰ PDF ุงุญุชุฑุงููŠ ุจุฒุฑ ูˆุงุญุฏ |
1018
+ | ๐ŸŒ ุจุญุซ ุนุงู„ู…ูŠ | ุชุจูˆูŠุจ ู…ุณุชู‚ู„ ู„ู„ุจุญุซ ุนู† ุฃูŠ ูˆุฑู‚ุฉ ุจุฏูˆู† ุฌู„ุจ ู…ุณุจู‚ |
1019
 
1020
+ ### ๐Ÿ”ง ุฅุตู„ุงุญุงุช v7.2 (ู…ูุถู…ูŽู‘ู†ุฉ)
1021
  | ุงู„ู…ุดูƒู„ุฉ | ุงู„ุฅุตู„ุงุญ |
1022
  |---|---|
1023
+ | ุชูˆุงุฑูŠุฎ CrossRef ู…ุฒูŠูุฉ (2048ุŒ 2116...) | `parse_crossref_date()` ุชุฑูุถ ุฃูŠ ุณู†ุฉ ุฎุงุฑุฌ `[1900, 2027]` |
1024
+ | ุฅุนุงุฏุฉ ุชุดุบูŠู„ ุนู†ุฏ ุงู„ูู„ุชุฑ | Filter โ†’ 3 outputs ูู‚ุท (ู„ุง cascade) |
 
 
 
 
 
 
 
 
 
 
 
1025
  """)
1026
 
1027
  # โ”€โ”€ WIRING โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
1032
  outputs=FETCH_OUT)
1033
  btn_filter.click(gr_filter_papers,
1034
  inputs=[f_year_from, f_year_to, f_cit_min, f_cit_max, f_sort],
1035
+ outputs=[papers_table_md, paper_selector, status_bar])
1036
  paper_selector.change(lambda x: [gr.update(value=x)]*3,
1037
  inputs=[paper_selector], outputs=[paper_sel2, cmp_a, cmp_b])
1038
 
1039
  btn_search_in.click(gr_search_fetched, inputs=[search_in_box], outputs=[search_in_out])
1040
  search_in_box.submit(gr_search_fetched, inputs=[search_in_box], outputs=[search_in_out])
1041
 
1042
+ # Global Search wiring
1043
+ btn_gs.click(global_paper_search, inputs=[gs_query, gs_source, gs_max], outputs=[gs_out])
1044
+ gs_query.submit(global_paper_search, inputs=[gs_query, gs_source, gs_max], outputs=[gs_out])
1045
+
1046
+ # Explain wiring
1047
  btn_explain.click(gr_explain, inputs=[paper_sel2, lang_exp], outputs=[explanation_out])
1048
  btn_fav.click(gr_save_fav, inputs=[paper_sel2], outputs=[fav_status])
1049
  btn_audio.click(gr_audio, inputs=[explanation_out, lang_exp], outputs=[audio_out])
1050
+ btn_export_pdf.click(gr_export_pdf,
1051
+ inputs=[explanation_out, paper_sel2],
1052
+ outputs=[pdf_out, pdf_status]) # โœ… NEW
1053
 
1054
  btn_compare.click(gr_compare, inputs=[cmp_a, cmp_b, lang_cmp], outputs=[compare_out])
1055
  btn_overview.click(gr_overview, inputs=[t_query, lang_ov], outputs=[overview_out])