arionxiv 1.0.32__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. arionxiv/__init__.py +40 -0
  2. arionxiv/__main__.py +10 -0
  3. arionxiv/arxiv_operations/__init__.py +0 -0
  4. arionxiv/arxiv_operations/client.py +225 -0
  5. arionxiv/arxiv_operations/fetcher.py +173 -0
  6. arionxiv/arxiv_operations/searcher.py +122 -0
  7. arionxiv/arxiv_operations/utils.py +293 -0
  8. arionxiv/cli/__init__.py +4 -0
  9. arionxiv/cli/commands/__init__.py +1 -0
  10. arionxiv/cli/commands/analyze.py +587 -0
  11. arionxiv/cli/commands/auth.py +365 -0
  12. arionxiv/cli/commands/chat.py +714 -0
  13. arionxiv/cli/commands/daily.py +482 -0
  14. arionxiv/cli/commands/fetch.py +217 -0
  15. arionxiv/cli/commands/library.py +295 -0
  16. arionxiv/cli/commands/preferences.py +426 -0
  17. arionxiv/cli/commands/search.py +254 -0
  18. arionxiv/cli/commands/settings_unified.py +1407 -0
  19. arionxiv/cli/commands/trending.py +41 -0
  20. arionxiv/cli/commands/welcome.py +168 -0
  21. arionxiv/cli/main.py +407 -0
  22. arionxiv/cli/ui/__init__.py +1 -0
  23. arionxiv/cli/ui/global_theme_manager.py +173 -0
  24. arionxiv/cli/ui/logo.py +127 -0
  25. arionxiv/cli/ui/splash.py +89 -0
  26. arionxiv/cli/ui/theme.py +32 -0
  27. arionxiv/cli/ui/theme_system.py +391 -0
  28. arionxiv/cli/utils/__init__.py +54 -0
  29. arionxiv/cli/utils/animations.py +522 -0
  30. arionxiv/cli/utils/api_client.py +583 -0
  31. arionxiv/cli/utils/api_config.py +505 -0
  32. arionxiv/cli/utils/command_suggestions.py +147 -0
  33. arionxiv/cli/utils/db_config_manager.py +254 -0
  34. arionxiv/github_actions_runner.py +206 -0
  35. arionxiv/main.py +23 -0
  36. arionxiv/prompts/__init__.py +9 -0
  37. arionxiv/prompts/prompts.py +247 -0
  38. arionxiv/rag_techniques/__init__.py +8 -0
  39. arionxiv/rag_techniques/basic_rag.py +1531 -0
  40. arionxiv/scheduler_daemon.py +139 -0
  41. arionxiv/server.py +1000 -0
  42. arionxiv/server_main.py +24 -0
  43. arionxiv/services/__init__.py +73 -0
  44. arionxiv/services/llm_client.py +30 -0
  45. arionxiv/services/llm_inference/__init__.py +58 -0
  46. arionxiv/services/llm_inference/groq_client.py +469 -0
  47. arionxiv/services/llm_inference/llm_utils.py +250 -0
  48. arionxiv/services/llm_inference/openrouter_client.py +564 -0
  49. arionxiv/services/unified_analysis_service.py +872 -0
  50. arionxiv/services/unified_auth_service.py +457 -0
  51. arionxiv/services/unified_config_service.py +456 -0
  52. arionxiv/services/unified_daily_dose_service.py +823 -0
  53. arionxiv/services/unified_database_service.py +1633 -0
  54. arionxiv/services/unified_llm_service.py +366 -0
  55. arionxiv/services/unified_paper_service.py +604 -0
  56. arionxiv/services/unified_pdf_service.py +522 -0
  57. arionxiv/services/unified_prompt_service.py +344 -0
  58. arionxiv/services/unified_scheduler_service.py +589 -0
  59. arionxiv/services/unified_user_service.py +954 -0
  60. arionxiv/utils/__init__.py +51 -0
  61. arionxiv/utils/api_helpers.py +200 -0
  62. arionxiv/utils/file_cleanup.py +150 -0
  63. arionxiv/utils/ip_helper.py +96 -0
  64. arionxiv-1.0.32.dist-info/METADATA +336 -0
  65. arionxiv-1.0.32.dist-info/RECORD +69 -0
  66. arionxiv-1.0.32.dist-info/WHEEL +5 -0
  67. arionxiv-1.0.32.dist-info/entry_points.txt +4 -0
  68. arionxiv-1.0.32.dist-info/licenses/LICENSE +21 -0
  69. arionxiv-1.0.32.dist-info/top_level.txt +1 -0
@@ -0,0 +1,426 @@
1
+ """Preferences command for ArionXiv CLI - Paper preferences management"""
2
+
3
+ import sys
4
+ import asyncio
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ # Add backend to Python path
9
+ backend_path = Path(__file__).parent.parent.parent
10
+ sys.path.insert(0, str(backend_path))
11
+
12
+ import click
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+ from rich.prompt import Prompt, Confirm
16
+ from rich.table import Table
17
+ from rich.text import Text
18
+
19
+ from ..utils.db_config_manager import db_config_manager
20
+ from ..ui.theme import create_themed_console, print_header, style_text, print_success, print_warning, print_error, get_theme_colors
21
+ from ...services.unified_user_service import unified_user_service
22
+
23
+ console = create_themed_console()
24
+ logger = logging.getLogger(__name__)
25
+
26
+ @click.command()
27
+ @click.option('--categories', '-c', is_flag=True, help='Configure preferred categories')
28
+ @click.option('--keywords', '-k', is_flag=True, help='Configure keywords')
29
+ @click.option('--authors', '-a', is_flag=True, help='Configure preferred authors')
30
+ @click.option('--schedule', '-t', is_flag=True, help='Configure daily dose schedule')
31
+ @click.option('--show', '-s', is_flag=True, help='Show current preferences')
32
+ @click.option('--reset', '-r', is_flag=True, help='Reset to default preferences')
33
+ def preferences_command(categories: bool, keywords: bool, authors: bool, schedule: bool, show: bool, reset: bool):
34
+ """
35
+ Configure your paper preferences for daily dose
36
+
37
+ Examples:
38
+ \b
39
+ arionxiv preferences --show
40
+ arionxiv preferences --categories
41
+ arionxiv preferences --keywords
42
+ arionxiv preferences --authors
43
+ arionxiv preferences --schedule
44
+ arionxiv preferences --reset
45
+ """
46
+
47
+ async def _handle_preferences():
48
+ # Lazy import to avoid circular dependencies
49
+ from ...services.unified_config_service import unified_config_service
50
+
51
+ print_header(console, "Paper Preferences")
52
+
53
+ # Check authentication
54
+ if not unified_user_service.is_authenticated():
55
+ print_error(console, "ERROR: You must be logged in to manage preferences")
56
+ return
57
+
58
+ user = unified_user_service.get_current_user()
59
+ user_id = user["id"]
60
+
61
+ if show:
62
+ await _show_preferences(user_id)
63
+ elif categories:
64
+ await _configure_categories(user_id)
65
+ elif keywords:
66
+ await _configure_keywords(user_id)
67
+ elif authors:
68
+ await _configure_authors(user_id)
69
+ elif schedule:
70
+ await _configure_schedule(user_id)
71
+ elif reset:
72
+ await _reset_preferences(user_id)
73
+ else:
74
+ await _show_preferences_menu(user_id)
75
+
76
+ # Run async function
77
+ asyncio.run(_handle_preferences())
78
+
79
+ async def _show_preferences(user_id: str):
80
+ """Show current user preferences"""
81
+ # Import here to avoid circular dependencies
82
+ from ...services.unified_config_service import unified_config_service
83
+
84
+ console.print(f"\n{style_text('Current Paper Preferences', 'primary')}")
85
+ console.rule(style=f"bold {get_theme_colors()['primary']}")
86
+
87
+ result = await unified_config_service.get_user_preferences(user_id)
88
+
89
+ if not result["success"]:
90
+ print_error(console, f"Failed to load preferences: {result['message']}")
91
+ return
92
+
93
+ prefs = result["preferences"]
94
+
95
+ # Categories
96
+ console.print(f"\n{style_text('Preferred Categories:', 'accent')}")
97
+ if prefs["categories"]:
98
+ available_cats = unified_config_service.get_available_categories()
99
+ for cat in prefs["categories"]:
100
+ cat_name = available_cats.get(cat, cat)
101
+ console.print(f" • {cat} - {cat_name}")
102
+ else:
103
+ console.print(" No categories selected")
104
+
105
+ # Keywords
106
+ console.print(f"\n{style_text('Keywords:', 'accent')}")
107
+ if prefs["keywords"]:
108
+ for keyword in prefs["keywords"]:
109
+ console.print(f" • {keyword}")
110
+ else:
111
+ console.print(" No keywords set")
112
+
113
+ # Authors
114
+ console.print(f"\n{style_text('Preferred Authors:', 'accent')}")
115
+ if prefs["authors"]:
116
+ for author in prefs["authors"]:
117
+ console.print(f" • {author}")
118
+ else:
119
+ console.print(" No preferred authors")
120
+
121
+ # Exclude keywords
122
+ console.print(f"\n{style_text('Exclude Keywords:', 'accent')}")
123
+ if prefs["exclude_keywords"]:
124
+ for keyword in prefs["exclude_keywords"]:
125
+ console.print(f" • {keyword}")
126
+ else:
127
+ console.print(" No exclusions")
128
+
129
+ # Settings
130
+ console.print(f"\n{style_text('Settings:', 'accent')}")
131
+ console.print(f" • Minimum relevance score: {prefs['min_relevance_score']}")
132
+ console.print(f" • Maximum papers per day: {prefs['max_papers_per_day']}")
133
+
134
+ # Daily dose schedule
135
+ console.print(f"\n{style_text('Daily Dose Schedule:', 'accent')}")
136
+ console.print(f" • Enabled: {'Yes' if prefs.get('daily_dose_enabled', False) else 'No'}")
137
+ console.print(f" • Time: {prefs.get('daily_dose_time', '08:00')} UTC")
138
+
139
+ async def _configure_categories(user_id: str):
140
+ """Configure preferred categories"""
141
+ # Import here to avoid circular dependencies
142
+ from ...services.unified_config_service import unified_config_service
143
+
144
+ console.print(f"\n{style_text('Configure Preferred Categories', 'primary')}")
145
+ console.rule(style=f"bold {get_theme_colors()['primary']}")
146
+
147
+ # Get current preferences
148
+ result = await unified_config_service.get_user_preferences(user_id)
149
+ if not result["success"]:
150
+ print_error(console, f"Failed to load preferences: {result['message']}")
151
+ return
152
+
153
+ current_categories = result["preferences"]["categories"]
154
+ available_categories = unified_config_service.get_available_categories()
155
+ colors = get_theme_colors()
156
+
157
+ # Show available categories in a table
158
+ console.print(f"\n{style_text('Available Categories:', 'info')}")
159
+
160
+ table = Table(show_header=True, header_style=f"bold {colors['primary']}")
161
+ table.add_column("ID", style="bold white", width=15)
162
+ table.add_column("Category", style="white")
163
+ table.add_column("Selected", style="white", width=10)
164
+
165
+ for cat_id, cat_name in available_categories.items():
166
+ selected = "[X]" if cat_id in current_categories else "[ ]"
167
+ table.add_row(cat_id, cat_name, selected)
168
+
169
+ console.print(table)
170
+
171
+ # Get user selections
172
+ console.print(f"\n{style_text('Current selections:', 'accent')} {', '.join(current_categories) if current_categories else 'None'}")
173
+
174
+ console.print(f"\n{style_text('Instructions:', 'info')}")
175
+ console.print("• Enter category IDs separated by commas (e.g., cs.AI, cs.LG, cs.CV)")
176
+ console.print("• Leave empty to clear all selections")
177
+ console.print("• Type 'cancel' to abort")
178
+
179
+ while True:
180
+ selection = Prompt.ask(
181
+ "\n[bold]Enter category IDs[/bold]",
182
+ default=", ".join(current_categories)
183
+ )
184
+
185
+ if selection.lower() == "cancel":
186
+ print_warning(console, "Category configuration cancelled")
187
+ return
188
+
189
+ if not selection.strip():
190
+ new_categories = []
191
+ break
192
+
193
+ # Parse and validate categories
194
+ entered_cats = [cat.strip() for cat in selection.split(",")]
195
+ invalid_cats = [cat for cat in entered_cats if cat not in available_categories]
196
+
197
+ if invalid_cats:
198
+ print_error(console, f"Invalid categories: {', '.join(invalid_cats)}")
199
+ console.print("Please check the category IDs from the table above")
200
+ continue
201
+
202
+ new_categories = entered_cats
203
+ break
204
+
205
+ # Update preferences
206
+ current_prefs = result["preferences"]
207
+ current_prefs["categories"] = new_categories
208
+
209
+ save_result = await unified_config_service.save_user_preferences(user_id, current_prefs)
210
+
211
+ if save_result["success"]:
212
+ print_success(console, "Categories updated successfully!")
213
+ if new_categories:
214
+ console.print(f"Selected: {', '.join(new_categories)}")
215
+ else:
216
+ console.print("All categories cleared")
217
+ else:
218
+ print_error(console, f"Failed to save: {save_result['message']}")
219
+
220
+ async def _configure_keywords(user_id: str):
221
+ """Configure keywords"""
222
+ # Import here to avoid circular dependencies
223
+ from ...services.unified_config_service import unified_config_service
224
+
225
+ console.print(f"\n{style_text('Configure Keywords', 'primary')}")
226
+ console.rule(style=f"bold {get_theme_colors()['primary']}")
227
+
228
+ # Get current preferences
229
+ result = await unified_config_service.get_user_preferences(user_id)
230
+ if not result["success"]:
231
+ print_error(console, f"Failed to load preferences: {result['message']}")
232
+ return
233
+
234
+ current_prefs = result["preferences"]
235
+
236
+ console.print(f"\n{style_text('Current keywords:', 'accent')} {', '.join(current_prefs['keywords']) if current_prefs['keywords'] else 'None'}")
237
+ console.print(f"{style_text('Current exclusions:', 'accent')} {', '.join(current_prefs['exclude_keywords']) if current_prefs['exclude_keywords'] else 'None'}")
238
+
239
+ console.print(f"\n{style_text('Instructions:', 'info')}")
240
+ console.print("• Keywords help find relevant papers in titles and abstracts")
241
+ console.print("• Enter keywords separated by commas")
242
+ console.print("• Use specific terms like 'neural networks', 'deep learning', 'transformer'")
243
+
244
+ # Configure include keywords
245
+ include_keywords = Prompt.ask(
246
+ "\n[bold]Include keywords[/bold]",
247
+ default=", ".join(current_prefs["keywords"])
248
+ )
249
+
250
+ # Configure exclude keywords
251
+ exclude_keywords = Prompt.ask(
252
+ "\n[bold]Exclude keywords (papers with these will be filtered out)[/bold]",
253
+ default=", ".join(current_prefs["exclude_keywords"])
254
+ )
255
+
256
+ # Process keywords
257
+ new_include = [kw.strip() for kw in include_keywords.split(",") if kw.strip()] if include_keywords else []
258
+ new_exclude = [kw.strip() for kw in exclude_keywords.split(",") if kw.strip()] if exclude_keywords else []
259
+
260
+ # Update preferences
261
+ current_prefs["keywords"] = new_include
262
+ current_prefs["exclude_keywords"] = new_exclude
263
+
264
+ save_result = await unified_config_service.save_user_preferences(user_id, current_prefs)
265
+
266
+ if save_result["success"]:
267
+ print_success(console, "Keywords updated successfully!")
268
+ if new_include:
269
+ console.print(f"Include: {', '.join(new_include)}")
270
+ if new_exclude:
271
+ console.print(f"Exclude: {', '.join(new_exclude)}")
272
+ else:
273
+ print_error(console, f"Failed to save: {save_result['message']}")
274
+
275
+ async def _configure_authors(user_id: str):
276
+ """Configure preferred authors"""
277
+ from ...services.unified_config_service import unified_config_service
278
+
279
+ console.print(f"\n{style_text('Configure Preferred Authors', 'primary')}")
280
+ console.rule(style=f"bold {get_theme_colors()['primary']}")
281
+
282
+ result = await unified_config_service.get_user_preferences(user_id)
283
+ if not result["success"]:
284
+ print_error(console, f"Failed to load preferences: {result['message']}")
285
+ return
286
+
287
+ current_prefs = result["preferences"]
288
+
289
+ console.print(f"\n{style_text('Current authors:', 'accent')} {', '.join(current_prefs['authors']) if current_prefs['authors'] else 'None'}")
290
+
291
+ console.print(f"\n{style_text('Instructions:', 'info')}")
292
+ console.print("• Add authors whose papers you want to prioritize")
293
+ console.print("• Enter names separated by commas")
294
+ console.print("• Use last names or full names (e.g., 'Hinton', 'Geoffrey Hinton')")
295
+
296
+ authors_input = Prompt.ask(
297
+ "\n[bold]Preferred authors[/bold]",
298
+ default=", ".join(current_prefs["authors"])
299
+ )
300
+
301
+ new_authors = [auth.strip() for auth in authors_input.split(",") if auth.strip()] if authors_input else []
302
+
303
+ current_prefs["authors"] = new_authors
304
+
305
+ save_result = await unified_config_service.save_user_preferences(user_id, current_prefs)
306
+
307
+ if save_result["success"]:
308
+ print_success(console, "Authors updated successfully!")
309
+ if new_authors:
310
+ console.print(f"Preferred authors: {', '.join(new_authors)}")
311
+ else:
312
+ print_error(console, f"Failed to save: {save_result['message']}")
313
+
314
+ async def _configure_schedule(user_id: str):
315
+ """Configure daily dose schedule"""
316
+ from ...services.unified_config_service import unified_config_service
317
+
318
+ console.print(f"\n{style_text('Configure Daily Dose Schedule', 'primary')}")
319
+ console.rule(style=f"bold {get_theme_colors()['primary']}")
320
+
321
+ result = await unified_config_service.get_user_preferences(user_id)
322
+ if not result["success"]:
323
+ print_error(console, f"Failed to load preferences: {result['message']}")
324
+ return
325
+
326
+ current_prefs = result["preferences"]
327
+ current_enabled = current_prefs.get('daily_dose_enabled', False)
328
+ current_time = current_prefs.get('daily_dose_time', '08:00')
329
+
330
+ console.print(f"\n{style_text('Current settings:', 'accent')}")
331
+ console.print(f" • Status: {'Enabled' if current_enabled else 'Disabled'}")
332
+ console.print(f" • Time: {current_time} UTC")
333
+
334
+ console.print(f"\n{style_text('Note:', 'info')}")
335
+ console.print("• The scheduler daemon must be running in the cloud for this to work")
336
+ console.print("• See DEPLOYMENT.txt for deployment instructions")
337
+ console.print("• Time is in UTC timezone (24-hour format)")
338
+
339
+ enable_schedule = Confirm.ask(
340
+ "\n[bold]Enable daily dose schedule?[/bold]",
341
+ default=current_enabled
342
+ )
343
+
344
+ if enable_schedule:
345
+ console.print(f"\n{style_text('Time format:', 'info')} HH:MM (e.g., 08:00 for 8 AM, 14:30 for 2:30 PM)")
346
+
347
+ while True:
348
+ time_input = Prompt.ask(
349
+ "\n[bold]Daily dose time (UTC)[/bold]",
350
+ default=current_time
351
+ )
352
+
353
+ if len(time_input.split(":")) == 2:
354
+ try:
355
+ hour, minute = map(int, time_input.split(":"))
356
+ if 0 <= hour <= 23 and 0 <= minute <= 59:
357
+ current_prefs["daily_dose_time"] = time_input
358
+ break
359
+ else:
360
+ print_error(console, "Hour must be 0-23, minute must be 0-59")
361
+ except ValueError:
362
+ print_error(console, "Invalid time format. Use HH:MM (e.g., 08:00)")
363
+ else:
364
+ print_error(console, "Invalid time format. Use HH:MM (e.g., 08:00)")
365
+
366
+ current_prefs["daily_dose_enabled"] = enable_schedule
367
+
368
+ save_result = await unified_config_service.save_user_preferences(user_id, current_prefs)
369
+
370
+ if save_result["success"]:
371
+ print_success(console, "Schedule updated successfully!")
372
+ if enable_schedule:
373
+ console.print(f"Daily dose will be delivered at {current_prefs['daily_dose_time']} UTC")
374
+ console.print("\n[yellow]Make sure the scheduler daemon is deployed to the cloud[/yellow]")
375
+ else:
376
+ console.print("Daily dose schedule disabled")
377
+ else:
378
+ print_error(console, f"Failed to save: {save_result['message']}")
379
+
380
+ async def _reset_preferences(user_id: str):
381
+ """Reset preferences to default"""
382
+ # Import here to avoid circular dependencies
383
+ from ...services.unified_config_service import unified_config_service
384
+
385
+ if Confirm.ask("\n[bold red]Are you sure you want to reset all preferences to default?[/bold red]"):
386
+ default_prefs = unified_config_service._get_default_preferences()
387
+ result = await unified_config_service.save_user_preferences(user_id, default_prefs)
388
+
389
+ if result["success"]:
390
+ print_success(console, "Preferences reset to default!")
391
+ else:
392
+ print_error(console, f"Failed to reset: {result['message']}")
393
+ else:
394
+ print_warning(console, "Reset cancelled")
395
+
396
+ async def _show_preferences_menu(user_id: str):
397
+ """Show interactive preferences menu"""
398
+ await _show_preferences(user_id)
399
+
400
+ console.print(f"\n{style_text('Preferences Menu', 'primary')}")
401
+ console.rule(style=f"bold {get_theme_colors()['primary']}")
402
+
403
+ choices = [
404
+ ("1", "Configure Categories"),
405
+ ("2", "Configure Keywords"),
406
+ ("3", "Configure Authors"),
407
+ ("4", "Configure Schedule"),
408
+ ("5", "Reset to Defaults"),
409
+ ("q", "Quit")
410
+ ]
411
+
412
+ for key, desc in choices:
413
+ console.print(f" [{key}] {desc}")
414
+
415
+ choice = Prompt.ask("\n[bold]Select an option[/bold]", choices=[c[0] for c in choices], default="q")
416
+
417
+ if choice == "1":
418
+ await _configure_categories(user_id)
419
+ elif choice == "2":
420
+ await _configure_keywords(user_id)
421
+ elif choice == "3":
422
+ await _configure_authors(user_id)
423
+ elif choice == "4":
424
+ await _configure_schedule(user_id)
425
+ elif choice == "5":
426
+ await _reset_preferences(user_id)
@@ -0,0 +1,254 @@
1
+ """Search command for ArionXiv CLI"""
2
+
3
+ import sys
4
+ import asyncio
5
+ import subprocess
6
+ import logging
7
+ from pathlib import Path
8
+
9
+ # Add backend to Python path
10
+ backend_path = Path(__file__).parent.parent.parent
11
+ sys.path.insert(0, str(backend_path))
12
+
13
+ import click
14
+ from rich.console import Console
15
+
16
+ logger = logging.getLogger(__name__)
17
+ from rich.table import Table
18
+ from rich.progress import Progress, SpinnerColumn, TextColumn
19
+ from rich.panel import Panel
20
+ from rich.prompt import Prompt
21
+ from typing import Optional
22
+
23
+ from ...arxiv_operations.searcher import arxiv_searcher
24
+ from ..ui.theme import create_themed_console, create_themed_table, print_header, style_text, print_success, print_error, print_warning, get_theme_colors
25
+ from ..utils.animations import *
26
+ from ..utils.command_suggestions import show_command_suggestions
27
+
28
+ console = create_themed_console()
29
+ colors = get_theme_colors()
30
+
31
+
32
+ @click.command()
33
+ @click.argument('keywords', required=False, default=None)
34
+ @click.option('--category', '-c', help='Filter by category (e.g., cs.AI, cs.LG)')
35
+ @click.option('--author', '-a', help='Filter by author name')
36
+ def search_command(keywords: Optional[str], category: Optional[str], author: Optional[str]):
37
+ """
38
+ Search for research papers on arXiv.
39
+
40
+ Returns the top 10 matching papers for you to select from.
41
+
42
+ Examples:
43
+ \b
44
+ arionxiv search "transformer attention"
45
+ arionxiv search "neural networks" --category cs.LG
46
+ arionxiv search "deep learning" --author "Hinton"
47
+ """
48
+ # Check if keywords are provided
49
+ if not keywords and not author:
50
+ console.print(f"\n[bold {colors['primary']}]ArionXiv Paper Search[/bold {colors['primary']}]")
51
+ console.rule(style=f"bold {colors['primary']}")
52
+ console.print(f"\n[bold {colors['primary']}]Enter keywords to search for papers on arXiv.[/bold {colors['primary']}]")
53
+ console.print(f"[bold {colors['primary']}]Example: transformer attention, neural networks, deep learning[/bold {colors['primary']}]\n")
54
+
55
+ keywords = Prompt.ask(f"[bold {colors['primary']}]Search keywords[/bold {colors['primary']}]")
56
+
57
+ if not keywords.strip():
58
+ console.print(f"\n[bold {colors['warning']}]No keywords provided. Exiting.[/bold {colors['warning']}]")
59
+ return
60
+
61
+ asyncio.run(_search_papers(keywords or "", category, author))
62
+
63
+
64
+ async def _search_papers(keywords: str, category: Optional[str], author: Optional[str]):
65
+ """Execute the paper search"""
66
+
67
+ logger.info(f"Starting paper search: keywords='{keywords}', category={category}, author={author}")
68
+ left_to_right_reveal(console, f"Search: [bold {colors['primary']}]{keywords}[/bold {colors['primary']}]", style="bold", duration=1.0)
69
+
70
+ with Progress(
71
+ TextColumn(f"[bold {colors['primary']}]Searching arXiv...[/bold {colors['primary']}]"),
72
+ "[progress.bar]",
73
+ console=console
74
+ ) as progress:
75
+ task = progress.add_task("", total=100)
76
+ for i in range(100):
77
+ await asyncio.sleep(0.01)
78
+ progress.update(task, advance=1)
79
+
80
+ try:
81
+ # Perform search based on filters
82
+ if author:
83
+ logger.debug(f"Searching by author: {author}")
84
+ results = await arxiv_searcher.search_by_author(author=author, max_results=10)
85
+ elif category:
86
+ logger.debug(f"Searching by category: {category}")
87
+ results = await arxiv_searcher.search_by_category(query=keywords, category=category, max_results=10)
88
+ else:
89
+ logger.debug(f"Searching by keywords: {keywords}")
90
+ results = await arxiv_searcher.search(query=keywords, max_results=10)
91
+
92
+ progress.remove_task(task)
93
+
94
+ if not results["success"]:
95
+ logger.error(f"Search failed: {results.get('error', 'Unknown error')}")
96
+ print_error(console, f"Search failed: {results.get('error', 'Unknown error')}")
97
+ return
98
+
99
+ papers = results["papers"]
100
+ logger.info(f"Search completed: found {len(papers)} papers")
101
+
102
+ if not papers:
103
+ logger.warning(f"No papers found for: {keywords}")
104
+ print_warning(console, f"No papers found for: {keywords}")
105
+ return
106
+
107
+ except Exception as e:
108
+ logger.error(f"Search error: {str(e)}", exc_info=True)
109
+ progress.remove_task(task)
110
+ print_error(console, f"Search error: {str(e)}")
111
+ return
112
+
113
+ # Display results with animation
114
+ left_to_right_reveal(console, f"Found {len(papers)} papers", style=f"bold {colors['primary']}", duration=1.0)
115
+ console.print()
116
+
117
+ # Show results table with row-by-row animation
118
+ await _display_papers_table_animated(papers)
119
+
120
+ # Interactive selection
121
+ _show_selection_menu(papers)
122
+
123
+
124
+ async def _display_papers_table_animated(papers):
125
+ """Display papers table with row-by-row animation"""
126
+
127
+ def create_table_with_rows(num_rows: int) -> Table:
128
+ table = create_themed_table("Results")
129
+ table.expand = True
130
+ table.add_column("#", style=f"bold {colors['primary']}", width=4)
131
+ table.add_column("Title", style=f"bold {colors['primary']}")
132
+ table.add_column("Authors", style=f"bold {colors['primary']}", width=30)
133
+ table.add_column("Date", style=f"bold {colors['primary']}", width=12)
134
+
135
+ for i in range(num_rows):
136
+ paper = papers[i]
137
+ title = paper.get("title", "Unknown")
138
+ authors = paper.get("authors", [])
139
+ author_str = authors[0] + (f" +{len(authors)-1}" if len(authors) > 1 else "") if authors else "Unknown"
140
+ pub_date = paper.get("published", "")
141
+ if pub_date:
142
+ try:
143
+ from datetime import datetime
144
+ date_obj = datetime.fromisoformat(pub_date.replace('Z', '+00:00'))
145
+ date_str = date_obj.strftime("%Y-%m-%d")
146
+ except:
147
+ date_str = pub_date[:10] if len(pub_date) >= 10 else "Unknown"
148
+ else:
149
+ date_str = "Unknown"
150
+ table.add_row(str(i + 1), title, author_str, date_str)
151
+ return table
152
+
153
+ await row_by_row_table_reveal(console, create_table_with_rows, len(papers))
154
+
155
+
156
+ def _show_selection_menu(papers):
157
+ """Show selection menu for papers"""
158
+
159
+ left_to_right_reveal(console, f"Select a paper (1-{len(papers)}) or press Enter to exit:", style=f"bold {colors['primary']}", duration=1.0)
160
+ console.print()
161
+
162
+ choice = Prompt.ask("Paper number", default="")
163
+
164
+ if not choice:
165
+ return
166
+
167
+ try:
168
+ idx = int(choice) - 1
169
+ if idx < 0 or idx >= len(papers):
170
+ print_error(console, "Invalid selection")
171
+ return
172
+ except ValueError:
173
+ print_error(console, "Enter a valid number")
174
+ return
175
+
176
+ selected = papers[idx]
177
+ paper_id = selected.get("arxiv_id", "")
178
+
179
+ if not paper_id:
180
+ print_error(console, "Paper ID not available")
181
+ return
182
+
183
+ # Show selected paper details
184
+ console.print()
185
+ _display_paper_details(selected)
186
+
187
+ # Show actions
188
+ left_to_right_reveal(console, f"\nActions:", style=f"bold {colors['primary']}", duration=0.5)
189
+ left_to_right_reveal(console, f"1. Fetch (download PDF)", style=f"bold {colors['primary']}", duration=0.5)
190
+ left_to_right_reveal(console, f"2. Analyze (AI analysis)", style=f"bold {colors['primary']}", duration=0.5)
191
+ left_to_right_reveal(console, f"3. Chat (discuss the paper)", style=f"bold {colors['primary']}", duration=0.5)
192
+ left_to_right_reveal(console, f"4. Exit", style=f"bold {colors['primary']}", duration=0.5)
193
+
194
+ console.print()
195
+ action = Prompt.ask(f"[bold {colors['primary']}]Action[/bold {colors['primary']}]", choices=["1", "2", "3", "4"], default="4")
196
+
197
+ if action == "1":
198
+ left_to_right_reveal(console, f"\nFetching paper...", style=f"bold {colors['primary']}")
199
+ subprocess.run([sys.executable, "-m", "arionxiv", "fetch", paper_id])
200
+ elif action == "2":
201
+ left_to_right_reveal(console, f"\nAnalyzing paper...", style=f"bold {colors['primary']}")
202
+ subprocess.run([sys.executable, "-m", "arionxiv", "analyze", paper_id])
203
+ elif action == "3":
204
+ left_to_right_reveal(console, f"\nStarting chat...", style=f"bold {colors['primary']}")
205
+ subprocess.run([sys.executable, "-m", "arionxiv", "chat", "--paper-id", paper_id])
206
+ elif action == "4":
207
+ show_command_suggestions(console, context='search')
208
+
209
+
210
+ def _display_paper_details(paper):
211
+ """Display paper details"""
212
+
213
+ title = paper.get("title", "Unknown Title")
214
+
215
+ # Authors
216
+ authors = paper.get("authors", [])
217
+ author_str = ", ".join(authors) if isinstance(authors, list) else str(authors)
218
+
219
+ # Categories
220
+ categories = paper.get("categories", [])
221
+ categories_str = ', '.join(categories) if categories else ""
222
+
223
+ # Date
224
+ pub_date = paper.get("published", "Unknown")
225
+ if pub_date and len(pub_date) >= 10:
226
+ pub_date = pub_date[:10]
227
+
228
+ # arXiv ID
229
+ arxiv_id = paper.get("arxiv_id", "")
230
+
231
+ # Abstract
232
+ abstract = paper.get("abstract", "No abstract available")
233
+
234
+ # Build content for panel
235
+ content_lines = [
236
+ f"[bold {colors['primary']}]{title}[/bold {colors['primary']}]\n",
237
+ f"[bold {colors['primary']}]Authors:[/bold {colors['primary']}] {author_str}",
238
+ ]
239
+
240
+ if categories_str:
241
+ content_lines.append(f"[bold {colors['primary']}]Categories:[/bold {colors['primary']}] {categories_str}")
242
+
243
+ content_lines.append(f"[bold {colors['primary']}]Published:[/bold {colors['primary']}] {pub_date}")
244
+
245
+ if arxiv_id:
246
+ content_lines.append(f"[bold {colors['primary']}]arXiv ID:[/bold {colors['primary']}] {arxiv_id}")
247
+ content_lines.append(f"[bold {colors['primary']}]URL:[/bold {colors['primary']}] https://arxiv.org/abs/{arxiv_id}")
248
+
249
+ content_lines.append(f"\n[bold {colors['primary']}]Abstract:[/bold {colors['primary']}]")
250
+ content_lines.append(abstract)
251
+
252
+ content = "\n".join(content_lines)
253
+
254
+ console.print(Panel(content, title=f"[bold {colors['primary']}]Selected Paper[/bold {colors['primary']}]", border_style=f"bold {colors['primary']}"))