Subahdip627 commited on
Commit
4ffe16f
·
verified ·
1 Parent(s): ac68da3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +186 -727
app.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
- PaperSearchPro - STRICT Hierarchical Keyword Search
3
- PREMIUM GUI with Proper Dark/Light Text Contrast - FIXED
4
  """
5
 
6
  import requests
@@ -8,535 +8,147 @@ import pandas as pd
8
  import time
9
  from datetime import datetime
10
  import gradio as gr
11
- import tempfile
12
  import os
13
 
14
- # Custom CSS with proper contrast - BLACK TEXT ON WHITE BACKGROUNDS
 
 
 
15
  custom_css = """
16
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
17
 
18
  :root {
19
  --primary: #2563eb;
20
- --primary-dark: #1d4ed8;
21
  --secondary: #7c3aed;
22
- --success: #10b981;
23
- --warning: #f59e0b;
24
- --danger: #ef4444;
25
-
26
- /* Light theme - BLACK TEXT ON WHITE BACKGROUNDS */
27
- --bg-page: #ffffff;
28
- --bg-card: #ffffff;
29
- --bg-input: #ffffff;
30
- --bg-hover: #f3f4f6;
31
  --border: #e5e7eb;
32
-
33
- --text-primary: #000000;
34
- --text-secondary: #1f2937;
35
- --text-muted: #4b5563;
36
- --text-inverse: #ffffff;
37
-
38
- --shadow: 0 1px 3px rgba(0,0,0,0.1);
39
- --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1);
40
-
41
- --radius: 0.5rem;
42
- }
43
-
44
- /* Base styles - WHITE BACKGROUND, BLACK TEXT */
45
- .gradio-container {
46
- background-color: var(--bg-page) !important;
47
- font-family: 'Inter', -apple-system, sans-serif !important;
48
- color: #000000 !important;
49
  }
50
 
51
- /* Force ALL text to be BLACK */
52
- body, p, h1, h2, h3, h4, h5, h6, span, div, label, .gr-markdown,
53
- .gr-text, .gr-form-text, .gr-input-label, .gr-box-title,
54
- .gr-panel, .gr-group, .gr-block, .gr-form {
55
- color: #000000 !important;
56
- background-color: transparent !important;
57
  }
58
 
59
- /* Main container */
60
- .gradio-container-outer {
61
- background-color: #ffffff !important;
62
  }
63
 
64
- /* Cards - WHITE BACKGROUND */
65
  .white-card {
66
- background-color: #ffffff !important;
67
- border-radius: var(--radius) !important;
68
- padding: 1.5rem !important;
69
  border: 1px solid var(--border) !important;
70
- box-shadow: var(--shadow) !important;
71
- }
72
-
73
- /* Headers - BLACK */
74
- h1 {
75
- font-size: 2.5rem !important;
76
- font-weight: 700 !important;
77
- color: #000000 !important;
78
- background-color: transparent !important;
79
  }
80
 
81
  h1 span {
82
  background: linear-gradient(135deg, var(--primary), var(--secondary));
83
  -webkit-background-clip: text;
84
  -webkit-text-fill-color: transparent;
85
- background-color: transparent !important;
86
- }
87
-
88
- h3 {
89
- font-size: 1.25rem !important;
90
- font-weight: 600 !important;
91
- color: #000000 !important;
92
- margin-bottom: 1rem !important;
93
- background-color: transparent !important;
94
  }
95
 
96
- /* Input fields - WHITE BACKGROUND, BLACK TEXT */
97
- input, textarea, select, .gr-input, .gr-textarea, .gr-dropdown {
98
- background-color: #ffffff !important;
99
- border: 1px solid var(--border) !important;
100
- border-radius: var(--radius) !important;
101
- padding: 0.625rem 0.875rem !important;
102
- color: #000000 !important;
103
- font-size: 0.95rem !important;
104
- width: 100% !important;
105
- }
106
-
107
- input:focus, textarea:focus, select:focus {
108
- border-color: var(--primary) !important;
109
- outline: none !important;
110
- box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1) !important;
111
- }
112
-
113
- input::placeholder, textarea::placeholder {
114
- color: #6b7280 !important;
115
- opacity: 1 !important;
116
- }
117
-
118
- /* Labels - BLACK */
119
- label, .gr-label {
120
- color: #000000 !important;
121
- font-size: 0.875rem !important;
122
- font-weight: 600 !important;
123
- margin-bottom: 0.375rem !important;
124
- display: block !important;
125
- background-color: transparent !important;
126
- }
127
-
128
- /* Buttons */
129
- .gr-button-primary {
130
- background: linear-gradient(135deg, var(--primary), var(--secondary)) !important;
131
- border: none !important;
132
- color: #ffffff !important;
133
- font-weight: 600 !important;
134
- padding: 0.75rem 1.5rem !important;
135
- border-radius: var(--radius) !important;
136
- cursor: pointer !important;
137
- transition: all 0.2s !important;
138
- font-size: 1rem !important;
139
- width: 100% !important;
140
- }
141
-
142
- .gr-button-primary:hover {
143
- transform: translateY(-2px);
144
- box-shadow: var(--shadow-lg) !important;
145
- }
146
-
147
- .gr-button-secondary {
148
- background-color: #ffffff !important;
149
- border: 1px solid var(--border) !important;
150
- color: #000000 !important;
151
- font-weight: 500 !important;
152
- padding: 0.75rem 1.5rem !important;
153
- border-radius: var(--radius) !important;
154
- cursor: pointer !important;
155
- transition: all 0.2s !important;
156
- width: 100% !important;
157
- }
158
-
159
- .gr-button-secondary:hover {
160
- background-color: var(--bg-hover) !important;
161
- border-color: #000000 !important;
162
- }
163
-
164
- /* Table/Dataframe - WHITE BACKGROUND, BLACK TEXT */
165
- .gr-dataframe {
166
- background-color: #ffffff !important;
167
- border: 1px solid var(--border) !important;
168
- border-radius: var(--radius) !important;
169
- overflow: hidden !important;
170
- }
171
-
172
- .gr-dataframe table {
173
- width: 100% !important;
174
- border-collapse: collapse !important;
175
- background-color: #ffffff !important;
176
- }
177
-
178
- .gr-dataframe th {
179
- background-color: #f9fafb !important;
180
- color: #000000 !important;
181
- font-weight: 600 !important;
182
- font-size: 0.875rem !important;
183
- padding: 0.875rem !important;
184
- border-bottom: 2px solid var(--border) !important;
185
- text-align: left !important;
186
- }
187
-
188
- .gr-dataframe td {
189
- padding: 0.875rem !important;
190
- border-bottom: 1px solid var(--border) !important;
191
- color: #000000 !important;
192
- font-size: 0.875rem !important;
193
- background-color: #ffffff !important;
194
- }
195
-
196
- .gr-dataframe tr:last-child td {
197
- border-bottom: none !important;
198
  }
 
199
 
200
- .gr-dataframe tr:hover td {
201
- background-color: #f3f4f6 !important;
202
- }
203
 
204
- /* Slider */
205
- .gr-slider {
206
- background: linear-gradient(90deg, var(--primary), var(--secondary)) !important;
207
- height: 6px !important;
208
- border-radius: 3px !important;
209
- }
210
 
211
- .gr-slider-value {
212
- color: #000000 !important;
213
- font-weight: 600 !important;
214
- }
215
 
216
- /* Checkbox */
217
- .gr-checkbox {
218
- accent-color: var(--primary) !important;
219
- width: 1.125rem !important;
220
- height: 1.125rem !important;
221
- }
222
 
223
- .gr-checkbox-label {
224
- color: #000000 !important;
225
- margin-left: 0.5rem !important;
226
- }
 
 
 
227
 
228
- /* Metric cards - WHITE BACKGROUND, BLACK TEXT */
229
- .metric-card {
230
- background-color: #ffffff !important;
231
- border-radius: var(--radius) !important;
232
- padding: 1rem !important;
233
- border: 1px solid var(--border) !important;
234
- text-align: center !important;
235
- }
236
 
237
- .metric-value {
238
- font-size: 2rem !important;
239
- font-weight: 700 !important;
240
- color: var(--primary) !important;
241
- line-height: 1.2 !important;
242
- margin: 0 !important;
243
- background-color: transparent !important;
244
- }
245
 
246
- .metric-label {
247
- font-size: 0.875rem !important;
248
- color: #000000 !important;
249
- margin-top: 0.25rem !important;
250
- background-color: transparent !important;
251
- }
252
 
253
- /* Keyword badges */
254
- .keyword-badge {
255
- display: inline-block;
256
- padding: 0.375rem 1rem;
257
- border-radius: 9999px;
258
- font-size: 0.875rem;
259
- font-weight: 600;
260
- margin: 0.25rem;
261
- }
262
 
263
- .keyword-required {
264
- background-color: #fee2e2;
265
- color: #b91c1c !important;
266
- border: 1px solid #fecaca;
267
- }
268
 
269
- .keyword-optional {
270
- background-color: #dcfce7;
271
- color: #166534 !important;
272
- border: 1px solid #bbf7d0;
273
- }
274
 
275
- /* Info boxes - WHITE BACKGROUND */
276
- .info-box {
277
- background-color: #eff6ff !important;
278
- border-left: 4px solid var(--primary);
279
- border-radius: var(--radius);
280
- padding: 1rem;
281
- margin: 1rem 0;
282
- }
283
 
284
- .info-box strong, .info-box p {
285
- color: #000000 !important;
286
- }
287
 
288
- /* Status messages */
289
- .gr-status, .gr-message {
290
- color: #000000 !important;
291
- font-size: 0.875rem !important;
292
- padding: 0.5rem !important;
293
- background-color: #ffffff !important;
294
- }
295
 
296
- /* File download link */
297
- .gr-file {
298
- background-color: #ffffff !important;
299
- border: 1px solid var(--border) !important;
300
- border-radius: var(--radius) !important;
301
- padding: 0.5rem !important;
302
- }
303
 
304
- .gr-file a {
305
- color: var(--primary) !important;
306
- text-decoration: none !important;
307
- font-weight: 500 !important;
308
- }
309
 
310
- .gr-file a:hover {
311
- text-decoration: underline !important;
312
- }
313
 
314
- /* Number inputs */
315
- .gr-number {
316
- background-color: #ffffff !important;
317
- border: 1px solid var(--border) !important;
318
- border-radius: var(--radius) !important;
319
- padding: 0.5rem !important;
320
- color: #000000 !important;
321
- }
322
 
323
- /* Row and column layouts */
324
- .gr-row, .gr-column {
325
- background-color: transparent !important;
326
- }
327
 
328
- /* Tabs */
329
- .gr-tabs {
330
- background-color: #ffffff !important;
331
- }
332
 
333
- .gr-tabs .gr-tab-nav {
334
- background-color: #f9fafb !important;
335
- border-bottom: 1px solid var(--border) !important;
336
- }
337
 
338
- .gr-tabs .gr-tab-nav button {
339
- color: #000000 !important;
340
- background-color: transparent !important;
341
- }
342
 
343
- .gr-tabs .gr-tab-nav button.selected {
344
- color: var(--primary) !important;
345
- border-bottom: 2px solid var(--primary) !important;
346
- }
347
 
348
- /* Dropdown options */
349
- .gr-dropdown-options {
350
- background-color: #ffffff !important;
351
- border: 1px solid var(--border) !important;
352
- }
353
 
354
- .gr-dropdown-options .gr-dropdown-option {
355
- color: #000000 !important;
356
- }
357
 
358
- .gr-dropdown-options .gr-dropdown-option:hover {
359
- background-color: #f3f4f6 !important;
360
- }
361
 
362
- /* Responsive */
363
- @media (max-width: 768px) {
364
- .white-card {
365
- padding: 1rem !important;
366
- }
367
-
368
- h1 {
369
- font-size: 2rem !important;
370
- }
371
-
372
- .metric-value {
373
- font-size: 1.5rem !important;
374
- }
375
- }
376
- """
377
 
378
- class PaperSearchMobile:
379
- def __init__(self):
380
- self.base_url = "https://api.openalex.org/works"
381
- self.per_page = 200
382
-
383
- def search_papers(self, query, keywords, sort_by, sort_order, max_papers,
384
- year_from, year_to, open_access):
385
-
386
- # Prepare sort parameter
387
- sort_order_param = 'asc' if sort_order == "ascending" else 'desc'
388
- sort_field_map = {
389
- "citations": "cited_by_count",
390
- "year": "publication_year",
391
- "date": "publication_date",
392
- "title": "title",
393
- "relevance": "relevance_score"
394
- }
395
- api_sort_field = sort_field_map.get(sort_by, "cited_by_count")
396
- sort_param = f"{api_sort_field}:{sort_order_param}"
397
-
398
- # Prepare filters
399
- filters = []
400
- if year_from and year_from > 0:
401
- filters.append(f"publication_year:>{year_from-1}")
402
- if year_to and year_to > 0:
403
- filters.append(f"publication_year:<{year_to+1}")
404
- if open_access:
405
- filters.append("open_access.is_oa:true")
406
-
407
- filter_str = ",".join(filters) if filters else None
408
-
409
- all_papers = []
410
- page = 1
411
- max_attempts = 50
412
-
413
- # Fetch papers
414
- while len(all_papers) < max_papers and page <= max_attempts:
415
- params = {
416
- 'search': query,
417
- 'per-page': self.per_page,
418
- 'page': page,
419
- 'sort': sort_param
420
- }
421
-
422
- if filter_str:
423
- params['filter'] = filter_str
424
-
425
- try:
426
- response = requests.get(self.base_url, params=params, timeout=15)
427
- data = response.json()
428
- results = data.get("results", [])
429
-
430
- if not results:
431
- break
432
-
433
- for work in results:
434
- paper = self._extract_paper_info(work)
435
- all_papers.append(paper)
436
-
437
- page += 1
438
- time.sleep(0.3)
439
-
440
- except Exception as e:
441
- break
442
-
443
- # Strict hierarchical filtering
444
- keyword_list = []
445
- if keywords and keywords.strip():
446
- keyword_list = [k.strip().lower() for k in keywords.split(',') if k.strip()]
447
-
448
- if len(keyword_list) >= 2:
449
- scored_papers = []
450
- for paper in all_papers:
451
- title_text = paper['Title'].lower()
452
- abstract_text = paper.get('Abstract', '').lower()
453
- authors_text = paper['Authors'].lower()
454
-
455
- combined_text = f"{title_text} {abstract_text} {authors_text}"
456
-
457
- # Check if BOTH keywords are present
458
- kw1_present = keyword_list[0] in combined_text
459
- kw2_present = keyword_list[1] in combined_text
460
-
461
- if not (kw1_present and kw2_present):
462
- continue
463
-
464
- # Check abstract
465
- if (keyword_list[0] in abstract_text) or (keyword_list[1] in abstract_text):
466
- paper['abstract_match'] = "Yes"
467
- else:
468
- paper['abstract_match'] = "No"
469
-
470
- # Calculate score
471
- score = 2
472
- matched = [keyword_list[0], keyword_list[1]]
473
-
474
- for idx, kw in enumerate(keyword_list[2:], start=3):
475
- if kw in combined_text:
476
- score += (len(keyword_list) - idx + 1)
477
- matched.append(kw)
478
-
479
- if paper['abstract_match'] == "Yes":
480
- score += 1
481
-
482
- paper['Abstract_Short'] = paper['Abstract'][:200] + "..." if len(paper['Abstract']) > 200 else paper['Abstract']
483
- paper['score'] = score
484
- paper['matched'] = ', '.join(matched)
485
- paper['required_matched'] = f"✓ {keyword_list[0]}, ✓ {keyword_list[1]}"
486
-
487
- scored_papers.append(paper)
488
-
489
- scored_papers.sort(key=lambda x: x['score'], reverse=True)
490
- all_papers = scored_papers[:max_papers]
491
-
492
- # Prepare display
493
- if all_papers:
494
- for i, p in enumerate(all_papers):
495
- p['Rank'] = i + 1
496
-
497
- df = pd.DataFrame(all_papers)
498
- display_cols = ["Rank", "score", "Title", "Authors", "Year", "Citations", "Journal", "abstract_match", "Abstract_Short"]
499
- display_df = df[display_cols]
500
-
501
- rename_dict = {
502
- "score": "Relevance",
503
- "abstract_match": "In Abstract",
504
- "Abstract_Short": "Abstract Preview"
505
- }
506
- display_df = display_df.rename(columns=rename_dict)
507
-
508
- years_list = [p['Year'] for p in all_papers if p['Year'] != 'N/A']
509
-
510
- # Extract metrics for display
511
- total_papers = len(all_papers)
512
- if years_list:
513
- year_min = min(years_list)
514
- year_max = max(years_list)
515
- year_range_text = f"{year_min} - {year_max}"
516
- else:
517
- year_range_text = "N/A"
518
-
519
- if total_papers > 0:
520
- avg_relevance = sum(p.get('score', 0) for p in all_papers) / total_papers
521
- else:
522
- avg_relevance = 0
523
-
524
- return display_df, all_papers, total_papers, avg_relevance, year_range_text
525
-
526
- return pd.DataFrame(), [], 0, 0, "N/A"
527
-
528
- def _extract_paper_info(self, work):
529
- def get_authors(authorships):
530
- if not authorships:
531
- return "Unknown"
532
- authors = []
533
- for a in authorships[:3]:
534
- name = a.get("author", {}).get("display_name", "")
535
- if name:
536
- authors.append(name)
537
- return ", ".join(authors) + ("..." if len(authorships) > 3 else "")
538
-
539
- # Get FULL abstract
540
  abstract = ""
541
  if work.get("abstract_inverted_index"):
542
  words = []
@@ -547,289 +159,136 @@ class PaperSearchMobile:
547
  abstract = " ".join([w[1] for w in words])
548
  else:
549
  abstract = "No abstract available"
550
-
 
 
 
 
 
551
  return {
552
  "Title": work.get("title", "No title"),
553
- "Authors": get_authors(work.get("authorships", [])),
554
  "Year": work.get("publication_year", "N/A"),
555
  "Citations": work.get("cited_by_count", 0),
556
  "Journal": (work.get("primary_location", {}).get("source") or {}).get("display_name", "Unknown"),
557
- "DOI": work.get("doi", "N/A"),
558
- "URL": work.get("id", "N/A"),
559
  "Abstract": abstract,
560
- "matched": ""
 
561
  }
562
 
563
- def download_results(papers, fname, fmt):
564
- if not papers:
565
- return None, "No papers to export"
566
-
567
- df = pd.DataFrame(papers)
568
- column_order = ['Rank', 'score', 'Title', 'Authors', 'Year', 'Citations', 'Journal',
569
- 'DOI', 'URL', 'Abstract', 'matched', 'required_matched', 'abstract_match']
570
-
571
- available_cols = [col for col in column_order if col in df.columns]
572
- df = df[available_cols]
573
-
574
- df = df.rename(columns={
575
- 'score': 'Relevance_Score',
576
- 'matched': 'All_Keywords_Found',
577
- 'required_matched': 'Required_Keywords',
578
- 'abstract_match': 'Keywords_In_Abstract'
579
- })
580
-
581
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
582
-
583
- with tempfile.NamedTemporaryFile(mode='w', suffix=f'.{fmt.lower()}', delete=False) as tmp:
584
- if fmt == "TXT":
585
- filename = f"{fname}_with_abstracts_{timestamp}.txt"
586
- with open(tmp.name, 'w', encoding='utf-8') as f:
587
- # Write header
588
- f.write("="*100 + "\n")
589
- f.write(f"PAPER SEARCH RESULTS - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
590
- f.write("="*100 + "\n\n")
591
-
592
- # Write summary
593
- f.write("SUMMARY\n")
594
- f.write("-"*50 + "\n")
595
- f.write(f"Total Papers: {len(papers)}\n")
596
- f.write(f"Export Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
597
- f.write(f"Format: {fmt}\n\n")
598
-
599
- # Write each paper
600
- for idx, paper in enumerate(papers, 1):
601
- f.write(f"\n{'='*100}\n")
602
- f.write(f"PAPER #{idx}\n")
603
- f.write(f"{'='*100}\n")
604
-
605
- for col in available_cols:
606
- display_name = col.replace('_', ' ').title()
607
- value = paper.get(col, 'N/A')
608
- f.write(f"{display_name}: {value}\n")
609
-
610
- f.write("\n")
611
-
612
- elif fmt == "CSV":
613
- filename = f"{fname}_with_abstracts_{timestamp}.csv"
614
- df.to_csv(tmp.name, index=False, encoding='utf-8')
615
- else: # Excel
616
- filename = f"{fname}_with_abstracts_{timestamp}.xlsx"
617
- with pd.ExcelWriter(tmp.name, engine='openpyxl') as writer:
618
- df.to_excel(writer, sheet_name='Results', index=False)
619
-
620
- summary_data = {
621
- 'Metric': ['Total Papers', 'Date Exported', 'Export Format'],
622
- 'Value': [len(papers), datetime.now().strftime('%Y-%m-%d %H:%M:%S'), fmt]
623
- }
624
- pd.DataFrame(summary_data).to_excel(writer, sheet_name='Summary', index=False)
625
-
626
- return tmp.name, f"✅ Ready to download: {filename}"
627
 
628
  # ============================================
629
- # PREMIUM GRADIO INTERFACE
630
  # ============================================
 
631
  def create_interface():
632
- with gr.Blocks(title="PaperSearchPro", theme=gr.themes.Soft(), css=custom_css) as demo:
633
- # Header
 
634
  gr.HTML("""
635
- <div style='text-align: center; padding: 2rem 0; background-color: white;'>
636
  <h1><span>📱 PaperSearchPro</span></h1>
637
- <p style='color: #000000; font-size: 1.1rem; background-color: transparent;'>Academic Paper Search with Hierarchical Keywords</p>
638
  </div>
639
  """)
640
-
641
- # Main content
642
  with gr.Column(elem_classes="white-card"):
643
- # Search section
644
- with gr.Row():
645
- with gr.Column(scale=2):
646
- query = gr.Textbox(
647
- label="🔍 Search Topic",
648
- placeholder="Enter your research topic...",
649
- value="photoacoustic imaging",
650
- elem_classes="gr-input"
651
- )
652
-
653
- with gr.Column(scale=1):
654
- max_papers = gr.Slider(
655
- label="📊 Max Papers",
656
- minimum=100,
657
- maximum=10000,
658
- value=2000,
659
- step=100,
660
- elem_classes="gr-slider"
661
- )
662
-
663
- # Keywords section
664
- gr.HTML("""
665
- <div style='margin: 1.5rem 0; background-color: transparent;'>
666
- <h3 style='color: #000000; margin-bottom: 0.75rem; background-color: transparent;'>🔑 Keyword Hierarchy</h3>
667
- <div>
668
- <span class='keyword-badge keyword-required'>Keyword 1: REQUIRED</span>
669
- <span class='keyword-badge keyword-required'>Keyword 2: REQUIRED</span>
670
- <span class='keyword-badge keyword-optional'>Keyword 3+: Optional</span>
671
- </div>
672
- </div>
673
- """)
674
-
675
  keywords = gr.Textbox(
676
- label="Enter keywords (comma-separated)",
677
- placeholder="blood, photoacoustics, imaging, cancer",
678
- value="blood, photoacoustics, imaging",
679
- elem_classes="gr-input"
680
  )
681
-
682
- # Filters
683
- with gr.Row():
684
- with gr.Column():
685
- sort_by = gr.Dropdown(
686
- label="Sort By",
687
- choices=["relevance", "citations", "year"],
688
- value="relevance",
689
- elem_classes="gr-dropdown"
690
- )
691
-
692
- with gr.Column():
693
- sort_order = gr.Dropdown(
694
- label="Order",
695
- choices=["descending", "ascending"],
696
- value="descending",
697
- elem_classes="gr-dropdown"
698
- )
699
-
700
- with gr.Column():
701
- open_access = gr.Checkbox(
702
- label="Open Access Only",
703
- value=False,
704
- elem_classes="gr-checkbox"
705
- )
706
-
707
- # Year range
708
  with gr.Row():
709
- with gr.Column():
710
- year_from = gr.Number(
711
- label="Year From",
712
- value=2000,
713
- minimum=1900,
714
- maximum=2026,
715
- elem_classes="gr-number"
716
- )
717
-
718
- with gr.Column():
719
- year_to = gr.Number(
720
- label="Year To",
721
- value=2026,
722
- minimum=1900,
723
- maximum=2026,
724
- elem_classes="gr-number"
725
- )
726
-
727
- # Search button
728
- search_btn = gr.Button(
729
- "🔍 SEARCH PAPERS",
730
- variant="primary",
731
- size="lg",
732
- elem_classes="gr-button-primary"
733
- )
734
-
735
- # Metrics - now as display elements
736
  with gr.Row():
737
- with gr.Column():
738
- total_papers_display = gr.Markdown("**0**", elem_classes="metric-value")
739
- gr.Markdown("Total Papers", elem_classes="metric-label")
740
-
741
- with gr.Column():
742
- avg_relevance_display = gr.Markdown("**0.0**", elem_classes="metric-value")
743
- gr.Markdown("Avg Relevance", elem_classes="metric-label")
744
-
745
- with gr.Column():
746
- year_range_display = gr.Markdown("**N/A**", elem_classes="metric-value")
747
- gr.Markdown("Year Range", elem_classes="metric-label")
748
-
749
- # Results
750
- results = gr.Dataframe(
751
- label="📄 Results",
752
- wrap=True,
753
- row_count=15,
754
- elem_classes="gr-dataframe"
755
- )
756
- papers_state = gr.State([])
757
-
758
- # Export section
759
  with gr.Row():
760
- with gr.Column(scale=2):
761
- filename = gr.Textbox(
762
- label="Filename",
763
- value="papers_results",
764
- placeholder="Enter filename",
765
- elem_classes="gr-input"
766
- )
767
-
768
- with gr.Column(scale=1):
769
- format_type = gr.Dropdown(
770
- label="Format",
771
- choices=["TXT", "CSV", "Excel"],
772
- value="TXT",
773
- elem_classes="gr-dropdown"
774
- )
775
-
776
- export_btn = gr.Button(
777
- "📥 DOWNLOAD WITH ABSTRACTS",
778
- variant="secondary",
779
- elem_classes="gr-button-secondary"
780
- )
781
- export_status = gr.Textbox(label="", visible=True)
782
- download_file = gr.File(label="Download Link", visible=False)
783
-
784
- # Info box
785
- gr.HTML("""
786
- <div class='info-box'>
787
- <strong>📌 Note:</strong> Downloaded files include FULL abstracts.
788
- <strong>TXT</strong> format includes formatted text with summaries.
789
- <strong>CSV</strong> format is comma-separated values.
790
- <strong>Excel</strong> format includes a Summary sheet with metadata.
791
- </div>
792
- """)
793
-
794
- # Initialize searcher
795
- searcher = PaperSearchMobile()
796
-
797
- # Search function
798
- def search_wrapper(query, keywords, sort_by, sort_order, max_papers, year_from, year_to, open_access):
799
- df, papers, total, avg, year_range = searcher.search_papers(
800
- query, keywords, sort_by, sort_order, max_papers, year_from, year_to, open_access
801
- )
802
-
803
- total_md = f"**{total}**"
804
- avg_md = f"**{avg:.1f}**"
805
- year_md = f"**{year_range}**"
806
-
807
- return df, papers, total_md, avg_md, year_md
808
-
809
  search_btn.click(
810
  search_wrapper,
811
- inputs=[query, keywords, sort_by, sort_order, max_papers, year_from, year_to, open_access],
812
- outputs=[results, papers_state, total_papers_display, avg_relevance_display, year_range_display]
813
  )
814
-
815
- # Export function
816
- def export_wrapper(papers, fname, fmt):
817
  if not papers:
818
  return None, "No papers to export", gr.File(visible=False)
819
- file_path, message = download_results(papers, fname, fmt)
820
- return file_path, message, gr.File(visible=True)
821
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
822
  export_btn.click(
823
- export_wrapper,
824
- inputs=[papers_state, filename, format_type],
825
  outputs=[download_file, export_status, download_file]
826
  )
827
-
828
  return demo
829
 
 
830
  # ============================================
831
- # MAIN ENTRY POINT
832
  # ============================================
 
833
  if __name__ == "__main__":
834
  demo = create_interface()
835
- demo.launch(share=False)
 
1
  """
2
+ PaperSearchPro - FULL Professional Version
3
+ Hierarchical Keyword Search + Export + Responsive UI
4
  """
5
 
6
  import requests
 
8
  import time
9
  from datetime import datetime
10
  import gradio as gr
 
11
  import os
12
 
13
+ # ============================================
14
+ # CLEAN RESPONSIVE CSS
15
+ # ============================================
16
+
17
  custom_css = """
18
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
19
 
20
  :root {
21
  --primary: #2563eb;
 
22
  --secondary: #7c3aed;
 
 
 
 
 
 
 
 
 
23
  --border: #e5e7eb;
24
+ --bg-soft: #f9fafb;
25
+ --text-main: #111827;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  }
27
 
28
+ body, .gradio-container {
29
+ font-family: 'Inter', sans-serif !important;
30
+ background-color: #ffffff !important;
31
+ color: var(--text-main) !important;
 
 
32
  }
33
 
34
+ .gradio-container {
35
+ max-width: 1600px !important;
36
+ margin: auto !important;
37
  }
38
 
 
39
  .white-card {
40
+ background: #ffffff !important;
 
 
41
  border: 1px solid var(--border) !important;
42
+ border-radius: 12px !important;
43
+ padding: 2rem !important;
 
 
 
 
 
 
 
44
  }
45
 
46
  h1 span {
47
  background: linear-gradient(135deg, var(--primary), var(--secondary));
48
  -webkit-background-clip: text;
49
  -webkit-text-fill-color: transparent;
 
 
 
 
 
 
 
 
 
50
  }
51
 
52
+ .metric-value {
53
+ font-size: 2rem !important;
54
+ font-weight: 700 !important;
55
+ color: var(--primary) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  }
57
+ """
58
 
59
+ # ============================================
60
+ # SEARCH ENGINE
61
+ # ============================================
62
 
63
+ class PaperSearchPro:
 
 
 
 
 
64
 
65
+ def __init__(self):
66
+ self.base_url = "https://api.openalex.org/works"
67
+ self.per_page = 200
 
68
 
69
+ def search_papers(self, query, keywords, max_papers, year_from, year_to, open_access):
 
 
 
 
 
70
 
71
+ filters = []
72
+ if year_from:
73
+ filters.append(f"publication_year:>{int(year_from)-1}")
74
+ if year_to:
75
+ filters.append(f"publication_year:<{int(year_to)+1}")
76
+ if open_access:
77
+ filters.append("open_access.is_oa:true")
78
 
79
+ filter_str = ",".join(filters) if filters else None
 
 
 
 
 
 
 
80
 
81
+ all_papers = []
82
+ page = 1
 
 
 
 
 
 
83
 
84
+ while len(all_papers) < max_papers:
85
+ params = {
86
+ "search": query,
87
+ "per-page": self.per_page,
88
+ "page": page
89
+ }
90
 
91
+ if filter_str:
92
+ params["filter"] = filter_str
 
 
 
 
 
 
 
93
 
94
+ response = requests.get(self.base_url, params=params, timeout=20)
95
+ data = response.json()
96
+ results = data.get("results", [])
 
 
97
 
98
+ if not results:
99
+ break
 
 
 
100
 
101
+ for work in results:
102
+ paper = self.extract_info(work)
103
+ all_papers.append(paper)
 
 
 
 
 
104
 
105
+ page += 1
106
+ time.sleep(0.2)
 
107
 
108
+ # ===============================
109
+ # HIERARCHICAL KEYWORD FILTERING
110
+ # ===============================
 
 
 
 
111
 
112
+ keyword_list = [k.strip().lower() for k in keywords.split(",") if k.strip()]
 
 
 
 
 
 
113
 
114
+ if len(keyword_list) >= 2:
115
+ filtered = []
 
 
 
116
 
117
+ for p in all_papers:
118
+ combined = f"{p['Title']} {p['Abstract']} {p['Authors']}".lower()
 
119
 
120
+ # first 2 keywords REQUIRED
121
+ if keyword_list[0] not in combined or keyword_list[1] not in combined:
122
+ continue
 
 
 
 
 
123
 
124
+ score = 2
125
+ matched = [keyword_list[0], keyword_list[1]]
 
 
126
 
127
+ for kw in keyword_list[2:]:
128
+ if kw in combined:
129
+ score += 1
130
+ matched.append(kw)
131
 
132
+ p["Relevance"] = score
133
+ p["Matched Keywords"] = ", ".join(matched)
 
 
134
 
135
+ filtered.append(p)
 
 
 
136
 
137
+ filtered.sort(key=lambda x: x["Relevance"], reverse=True)
138
+ all_papers = filtered[:max_papers]
 
 
139
 
140
+ total = len(all_papers)
141
+ avg_rel = sum(p.get("Relevance", 0) for p in all_papers) / total if total else 0
142
+ years = [p["Year"] for p in all_papers if p["Year"] != "N/A"]
143
+ year_range = f"{min(years)} - {max(years)}" if years else "N/A"
 
144
 
145
+ df = pd.DataFrame(all_papers)
 
 
146
 
147
+ return df, all_papers, total, avg_rel, year_range
 
 
148
 
149
+ def extract_info(self, work):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
+ # Rebuild abstract
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  abstract = ""
153
  if work.get("abstract_inverted_index"):
154
  words = []
 
159
  abstract = " ".join([w[1] for w in words])
160
  else:
161
  abstract = "No abstract available"
162
+
163
+ authors = ", ".join(
164
+ a.get("author", {}).get("display_name", "")
165
+ for a in work.get("authorships", [])[:3]
166
+ )
167
+
168
  return {
169
  "Title": work.get("title", "No title"),
170
+ "Authors": authors,
171
  "Year": work.get("publication_year", "N/A"),
172
  "Citations": work.get("cited_by_count", 0),
173
  "Journal": (work.get("primary_location", {}).get("source") or {}).get("display_name", "Unknown"),
 
 
174
  "Abstract": abstract,
175
+ "Relevance": 0,
176
+ "Matched Keywords": ""
177
  }
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
  # ============================================
181
+ # INTERFACE
182
  # ============================================
183
+
184
  def create_interface():
185
+
186
+ with gr.Blocks(title="PaperSearchPro", css=custom_css) as demo:
187
+
188
  gr.HTML("""
189
+ <div style='text-align:center; padding:2rem 0'>
190
  <h1><span>📱 PaperSearchPro</span></h1>
191
+ <p>Professional Academic Paper Search</p>
192
  </div>
193
  """)
194
+
 
195
  with gr.Column(elem_classes="white-card"):
196
+
197
+ query = gr.Textbox(label="Search Topic", value="photoacoustic imaging")
198
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
  keywords = gr.Textbox(
200
+ label="Keywords (first 2 REQUIRED)",
201
+ value="blood, photoacoustics, imaging"
 
 
202
  )
203
+
204
+ max_papers = gr.Slider(100, 5000, value=1000, step=100, label="Max Papers")
205
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  with gr.Row():
207
+ year_from = gr.Number(value=2000, label="Year From")
208
+ year_to = gr.Number(value=2026, label="Year To")
209
+ open_access = gr.Checkbox(label="Open Access Only")
210
+
211
+ search_btn = gr.Button("🔍 SEARCH PAPERS", variant="primary")
212
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  with gr.Row():
214
+ total_display = gr.Markdown("**0**", elem_classes="metric-value")
215
+ avg_display = gr.Markdown("**0.0**", elem_classes="metric-value")
216
+ year_display = gr.Markdown("**N/A**", elem_classes="metric-value")
217
+
218
+ results = gr.Dataframe(row_count=15)
219
+ state = gr.State([])
220
+
221
+ # ==========================
222
+ # EXPORT SECTION
223
+ # ==========================
224
+
225
+ gr.Markdown("### 📥 Export Results")
226
+
 
 
 
 
 
 
 
 
 
227
  with gr.Row():
228
+ filename = gr.Textbox(label="Filename", value="papers_results")
229
+ format_type = gr.Dropdown(
230
+ label="Format",
231
+ choices=["TXT", "CSV", "Excel"],
232
+ value="TXT"
233
+ )
234
+
235
+ export_btn = gr.Button("Download Results", variant="secondary")
236
+
237
+ export_status = gr.Markdown("")
238
+ download_file = gr.File(visible=False)
239
+
240
+ searcher = PaperSearchPro()
241
+
242
+ def search_wrapper(q, k, m, yf, yt, oa):
243
+ df, papers, total, avg, yr = searcher.search_papers(q, k, m, yf, yt, oa)
244
+ return df, papers, f"**{total}**", f"**{avg:.2f}**", f"**{yr}**"
245
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  search_btn.click(
247
  search_wrapper,
248
+ inputs=[query, keywords, max_papers, year_from, year_to, open_access],
249
+ outputs=[results, state, total_display, avg_display, year_display]
250
  )
251
+
252
+ def export_results(papers, fname, fmt):
253
+
254
  if not papers:
255
  return None, "No papers to export", gr.File(visible=False)
256
+
257
+ df = pd.DataFrame(papers)
258
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
259
+
260
+ if fmt == "CSV":
261
+ path = f"{fname}_{timestamp}.csv"
262
+ df.to_csv(path, index=False)
263
+
264
+ elif fmt == "Excel":
265
+ path = f"{fname}_{timestamp}.xlsx"
266
+ df.to_excel(path, index=False)
267
+
268
+ else:
269
+ path = f"{fname}_{timestamp}.txt"
270
+ with open(path, "w", encoding="utf-8") as f:
271
+ for i, row in df.iterrows():
272
+ f.write("="*80 + "\n")
273
+ for col in df.columns:
274
+ f.write(f"{col}: {row[col]}\n")
275
+ f.write("\n")
276
+
277
+ return path, f"✅ File ready: {path}", gr.File(visible=True)
278
+
279
  export_btn.click(
280
+ export_results,
281
+ inputs=[state, filename, format_type],
282
  outputs=[download_file, export_status, download_file]
283
  )
284
+
285
  return demo
286
 
287
+
288
  # ============================================
289
+ # MAIN
290
  # ============================================
291
+
292
  if __name__ == "__main__":
293
  demo = create_interface()
294
+ demo.launch(inbrowser=True, height=1000)