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.
- arionxiv/__init__.py +40 -0
- arionxiv/__main__.py +10 -0
- arionxiv/arxiv_operations/__init__.py +0 -0
- arionxiv/arxiv_operations/client.py +225 -0
- arionxiv/arxiv_operations/fetcher.py +173 -0
- arionxiv/arxiv_operations/searcher.py +122 -0
- arionxiv/arxiv_operations/utils.py +293 -0
- arionxiv/cli/__init__.py +4 -0
- arionxiv/cli/commands/__init__.py +1 -0
- arionxiv/cli/commands/analyze.py +587 -0
- arionxiv/cli/commands/auth.py +365 -0
- arionxiv/cli/commands/chat.py +714 -0
- arionxiv/cli/commands/daily.py +482 -0
- arionxiv/cli/commands/fetch.py +217 -0
- arionxiv/cli/commands/library.py +295 -0
- arionxiv/cli/commands/preferences.py +426 -0
- arionxiv/cli/commands/search.py +254 -0
- arionxiv/cli/commands/settings_unified.py +1407 -0
- arionxiv/cli/commands/trending.py +41 -0
- arionxiv/cli/commands/welcome.py +168 -0
- arionxiv/cli/main.py +407 -0
- arionxiv/cli/ui/__init__.py +1 -0
- arionxiv/cli/ui/global_theme_manager.py +173 -0
- arionxiv/cli/ui/logo.py +127 -0
- arionxiv/cli/ui/splash.py +89 -0
- arionxiv/cli/ui/theme.py +32 -0
- arionxiv/cli/ui/theme_system.py +391 -0
- arionxiv/cli/utils/__init__.py +54 -0
- arionxiv/cli/utils/animations.py +522 -0
- arionxiv/cli/utils/api_client.py +583 -0
- arionxiv/cli/utils/api_config.py +505 -0
- arionxiv/cli/utils/command_suggestions.py +147 -0
- arionxiv/cli/utils/db_config_manager.py +254 -0
- arionxiv/github_actions_runner.py +206 -0
- arionxiv/main.py +23 -0
- arionxiv/prompts/__init__.py +9 -0
- arionxiv/prompts/prompts.py +247 -0
- arionxiv/rag_techniques/__init__.py +8 -0
- arionxiv/rag_techniques/basic_rag.py +1531 -0
- arionxiv/scheduler_daemon.py +139 -0
- arionxiv/server.py +1000 -0
- arionxiv/server_main.py +24 -0
- arionxiv/services/__init__.py +73 -0
- arionxiv/services/llm_client.py +30 -0
- arionxiv/services/llm_inference/__init__.py +58 -0
- arionxiv/services/llm_inference/groq_client.py +469 -0
- arionxiv/services/llm_inference/llm_utils.py +250 -0
- arionxiv/services/llm_inference/openrouter_client.py +564 -0
- arionxiv/services/unified_analysis_service.py +872 -0
- arionxiv/services/unified_auth_service.py +457 -0
- arionxiv/services/unified_config_service.py +456 -0
- arionxiv/services/unified_daily_dose_service.py +823 -0
- arionxiv/services/unified_database_service.py +1633 -0
- arionxiv/services/unified_llm_service.py +366 -0
- arionxiv/services/unified_paper_service.py +604 -0
- arionxiv/services/unified_pdf_service.py +522 -0
- arionxiv/services/unified_prompt_service.py +344 -0
- arionxiv/services/unified_scheduler_service.py +589 -0
- arionxiv/services/unified_user_service.py +954 -0
- arionxiv/utils/__init__.py +51 -0
- arionxiv/utils/api_helpers.py +200 -0
- arionxiv/utils/file_cleanup.py +150 -0
- arionxiv/utils/ip_helper.py +96 -0
- arionxiv-1.0.32.dist-info/METADATA +336 -0
- arionxiv-1.0.32.dist-info/RECORD +69 -0
- arionxiv-1.0.32.dist-info/WHEEL +5 -0
- arionxiv-1.0.32.dist-info/entry_points.txt +4 -0
- arionxiv-1.0.32.dist-info/licenses/LICENSE +21 -0
- 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']}"))
|