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,1407 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified Settings System for ArionXiv CLI - User-friendly configuration interface
|
|
3
|
+
Provides seamless access to all user settings with short, intuitive commands
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import logging
|
|
8
|
+
import asyncio
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from datetime import datetime, time
|
|
11
|
+
from typing import Dict, Any, List, Optional
|
|
12
|
+
|
|
13
|
+
# Add backend to Python path
|
|
14
|
+
backend_path = Path(__file__).parent.parent.parent
|
|
15
|
+
sys.path.insert(0, str(backend_path))
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.panel import Panel
|
|
20
|
+
from rich.prompt import Prompt, Confirm, IntPrompt
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
from rich.text import Text
|
|
23
|
+
from rich.columns import Columns
|
|
24
|
+
|
|
25
|
+
from ..utils.db_config_manager import db_config_manager
|
|
26
|
+
from ..ui.theme import create_themed_console, print_header, style_text, print_success, print_warning, print_error, get_theme_colors, set_theme_colors
|
|
27
|
+
from ..utils.command_suggestions import show_command_suggestions
|
|
28
|
+
from ..utils.api_config import api_config_manager, show_api_status
|
|
29
|
+
from ..utils.animations import left_to_right_reveal
|
|
30
|
+
# Note: theme_selector import will be conditional since it may not exist
|
|
31
|
+
try:
|
|
32
|
+
from ..ui.theme_system import run_theme_selection
|
|
33
|
+
except ImportError:
|
|
34
|
+
run_theme_selection = None
|
|
35
|
+
|
|
36
|
+
from ...services.unified_user_service import unified_user_service
|
|
37
|
+
from ...services.unified_config_service import unified_config_service
|
|
38
|
+
from ..utils.api_client import api_client, APIClientError
|
|
39
|
+
|
|
40
|
+
# Try to import schedule_user_daily_dose, fallback if not available
|
|
41
|
+
try:
|
|
42
|
+
from ...services.unified_scheduler_service import schedule_user_daily_dose
|
|
43
|
+
except ImportError:
|
|
44
|
+
schedule_user_daily_dose = None
|
|
45
|
+
|
|
46
|
+
console = create_themed_console()
|
|
47
|
+
|
|
48
|
+
# ================================
|
|
49
|
+
# CUSTOM ERROR HANDLING FOR SETTINGS
|
|
50
|
+
# ================================
|
|
51
|
+
|
|
52
|
+
class SettingsGroup(click.Group):
|
|
53
|
+
"""Custom Click group for settings with proper error handling for invalid subcommands"""
|
|
54
|
+
|
|
55
|
+
def invoke(self, ctx):
|
|
56
|
+
"""Override invoke to catch errors from subcommands"""
|
|
57
|
+
try:
|
|
58
|
+
return super().invoke(ctx)
|
|
59
|
+
except click.UsageError as e:
|
|
60
|
+
self._show_error(e, ctx)
|
|
61
|
+
raise SystemExit(1)
|
|
62
|
+
|
|
63
|
+
def _show_error(self, error, ctx):
|
|
64
|
+
"""Display themed error message for invalid subcommands"""
|
|
65
|
+
colors = get_theme_colors()
|
|
66
|
+
error_console = Console()
|
|
67
|
+
error_msg = str(error)
|
|
68
|
+
|
|
69
|
+
error_console.print()
|
|
70
|
+
error_console.print(f"[bold {colors['error']}]⚠ Invalid Settings Command[/bold {colors['error']}]")
|
|
71
|
+
error_console.print(f"[{colors['error']}]{error_msg}[/{colors['error']}]")
|
|
72
|
+
error_console.print()
|
|
73
|
+
|
|
74
|
+
# Show available subcommands
|
|
75
|
+
error_console.print(f"[bold white]Available 'settings' subcommands:[/bold white]")
|
|
76
|
+
for cmd_name in sorted(self.list_commands(ctx)):
|
|
77
|
+
cmd = self.get_command(ctx, cmd_name)
|
|
78
|
+
if cmd and not cmd.hidden:
|
|
79
|
+
help_text = cmd.get_short_help_str(limit=50)
|
|
80
|
+
error_console.print(f" [{colors['primary']}]{cmd_name}[/{colors['primary']}] {help_text}")
|
|
81
|
+
|
|
82
|
+
error_console.print()
|
|
83
|
+
error_console.print(f"Run [{colors['primary']}]arionxiv settings --help[/{colors['primary']}] for more information.")
|
|
84
|
+
error_console.print()
|
|
85
|
+
|
|
86
|
+
# ================================
|
|
87
|
+
# MAIN SETTINGS COMMAND
|
|
88
|
+
# ================================
|
|
89
|
+
|
|
90
|
+
@click.group(cls=SettingsGroup, invoke_without_command=True)
|
|
91
|
+
@click.pass_context
|
|
92
|
+
def settings(ctx):
|
|
93
|
+
"""
|
|
94
|
+
ArionXiv Settings - Configure your research experience
|
|
95
|
+
|
|
96
|
+
Quick access commands:
|
|
97
|
+
\b
|
|
98
|
+
arionxiv settings show # View all settings
|
|
99
|
+
arionxiv settings theme # Change theme color
|
|
100
|
+
arionxiv settings api # Configure API keys
|
|
101
|
+
arionxiv settings preferences # Configure paper preferences
|
|
102
|
+
arionxiv settings categories # Set research categories
|
|
103
|
+
arionxiv settings keywords # Manage keywords
|
|
104
|
+
arionxiv settings authors # Set preferred authors
|
|
105
|
+
arionxiv settings daily # Daily dose configuration
|
|
106
|
+
arionxiv settings time # Set daily analysis time
|
|
107
|
+
arionxiv settings papers # Manage saved papers
|
|
108
|
+
"""
|
|
109
|
+
if ctx.invoked_subcommand is None:
|
|
110
|
+
# Show available subcommands when called without arguments
|
|
111
|
+
colors = get_theme_colors()
|
|
112
|
+
console.print(f"\n[bold {colors['primary']}]ArionXiv Settings[/bold {colors['primary']}]")
|
|
113
|
+
console.rule(style=f"bold {colors['primary']}")
|
|
114
|
+
console.print(f"\n[bold {colors['primary']}]Available settings commands:[/bold {colors['primary']}]\n")
|
|
115
|
+
|
|
116
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
117
|
+
table.add_column("Command", style=f"bold {colors['primary']}")
|
|
118
|
+
table.add_column("Description", style="white")
|
|
119
|
+
|
|
120
|
+
for cmd_name in sorted(ctx.command.list_commands(ctx)):
|
|
121
|
+
cmd = ctx.command.get_command(ctx, cmd_name)
|
|
122
|
+
if cmd and not cmd.hidden:
|
|
123
|
+
help_text = cmd.get_short_help_str(limit=50)
|
|
124
|
+
table.add_row(f"arionxiv settings {cmd_name}", help_text)
|
|
125
|
+
|
|
126
|
+
console.print(table)
|
|
127
|
+
console.print(f"\n[bold {colors['primary']}]Example:[/bold {colors['primary']}] arionxiv settings theme\n")
|
|
128
|
+
|
|
129
|
+
# ================================
|
|
130
|
+
# SHOW ALL SETTINGS
|
|
131
|
+
# ================================
|
|
132
|
+
|
|
133
|
+
@settings.command('show')
|
|
134
|
+
@click.option('--detailed', '-d', is_flag=True, help='Show detailed configuration')
|
|
135
|
+
def show_settings(detailed: bool):
|
|
136
|
+
"""Show current ArionXiv settings overview"""
|
|
137
|
+
|
|
138
|
+
async def _show():
|
|
139
|
+
await _ensure_authenticated()
|
|
140
|
+
print_header(console, "ArionXiv Settings Overview")
|
|
141
|
+
|
|
142
|
+
# Load current configuration
|
|
143
|
+
await db_config_manager.load_config()
|
|
144
|
+
user = unified_user_service.get_current_user()
|
|
145
|
+
user_id = user['id']
|
|
146
|
+
|
|
147
|
+
# Get user preferences
|
|
148
|
+
prefs_result = await unified_user_service.get_user_preferences(user_id)
|
|
149
|
+
prefs = prefs_result.get('preferences', {}) if prefs_result['success'] else {}
|
|
150
|
+
|
|
151
|
+
# Theme & Display
|
|
152
|
+
theme_color = db_config_manager.get_theme_color()
|
|
153
|
+
colors = get_theme_colors()
|
|
154
|
+
|
|
155
|
+
if detailed:
|
|
156
|
+
await _show_detailed_settings(prefs, theme_color)
|
|
157
|
+
else:
|
|
158
|
+
await _show_compact_settings(prefs, theme_color)
|
|
159
|
+
|
|
160
|
+
# Quick actions
|
|
161
|
+
console.print(f"\n{style_text('Quick Actions:', 'primary')}")
|
|
162
|
+
console.print(f"- {style_text('arionxiv settings theme', 'primary')} - Change color theme")
|
|
163
|
+
console.print(f"- {style_text('arionxiv settings prefs', 'primary')} - Configure preferences")
|
|
164
|
+
console.print(f"- {style_text('arionxiv settings daily', 'primary')} - Daily dose settings")
|
|
165
|
+
|
|
166
|
+
asyncio.run(_show())
|
|
167
|
+
|
|
168
|
+
# ================================
|
|
169
|
+
# THEME CONFIGURATION
|
|
170
|
+
# ================================
|
|
171
|
+
|
|
172
|
+
@settings.command('theme')
|
|
173
|
+
@click.option('--color', type=click.Choice(['red', 'blue', 'green', 'purple', 'cyan', 'amber']), help='Set theme directly')
|
|
174
|
+
def theme_settings(color: Optional[str]):
|
|
175
|
+
"""Change ArionXiv color theme"""
|
|
176
|
+
|
|
177
|
+
async def _theme():
|
|
178
|
+
await _ensure_authenticated()
|
|
179
|
+
colors = get_theme_colors()
|
|
180
|
+
|
|
181
|
+
console.print()
|
|
182
|
+
left_to_right_reveal(console, "Theme Settings", style=f"bold {colors['primary']}", duration=1.0)
|
|
183
|
+
console.print()
|
|
184
|
+
|
|
185
|
+
current_theme = db_config_manager.get_theme_color()
|
|
186
|
+
left_to_right_reveal(console, f"Current: {current_theme.title()}", style=f"bold {colors['primary']}", duration=1.0)
|
|
187
|
+
console.print()
|
|
188
|
+
|
|
189
|
+
if color:
|
|
190
|
+
# Direct color change
|
|
191
|
+
if await db_config_manager.set_theme_color(color):
|
|
192
|
+
# Reload the theme immediately to update global colors
|
|
193
|
+
await db_config_manager.load_config()
|
|
194
|
+
# Force update the global THEME_COLORS
|
|
195
|
+
set_theme_colors(color)
|
|
196
|
+
# Get fresh colors after theme reload
|
|
197
|
+
new_colors = get_theme_colors()
|
|
198
|
+
left_to_right_reveal(console, f"Theme changed to {color.title()}", style=f"bold {new_colors['primary']}", duration=1.0)
|
|
199
|
+
console.print()
|
|
200
|
+
show_command_suggestions(console, context='settings')
|
|
201
|
+
else:
|
|
202
|
+
left_to_right_reveal(console, "Failed to save", style=f"bold {colors['error']}", duration=1.0)
|
|
203
|
+
else:
|
|
204
|
+
# Interactive theme selection
|
|
205
|
+
if run_theme_selection:
|
|
206
|
+
selected = run_theme_selection(console)
|
|
207
|
+
if await db_config_manager.set_theme_color(selected):
|
|
208
|
+
# Reload the theme immediately to update global colors
|
|
209
|
+
await db_config_manager.load_config()
|
|
210
|
+
# Force update the global THEME_COLORS
|
|
211
|
+
set_theme_colors(selected)
|
|
212
|
+
show_command_suggestions(console, context='settings')
|
|
213
|
+
else:
|
|
214
|
+
# Get fresh colors for error message
|
|
215
|
+
current_colors = get_theme_colors()
|
|
216
|
+
left_to_right_reveal(console, "Failed to save", style=f"bold {current_colors['error']}", duration=1.0)
|
|
217
|
+
|
|
218
|
+
asyncio.run(_theme())
|
|
219
|
+
|
|
220
|
+
# ================================
|
|
221
|
+
# PREFERENCES OVERVIEW
|
|
222
|
+
# ================================
|
|
223
|
+
|
|
224
|
+
@settings.command('preferences')
|
|
225
|
+
@click.option('--reset', is_flag=True, help='Reset all preferences to defaults')
|
|
226
|
+
def preferences_overview(reset: bool):
|
|
227
|
+
"""Configure paper preferences overview"""
|
|
228
|
+
|
|
229
|
+
async def _prefs():
|
|
230
|
+
await _ensure_authenticated()
|
|
231
|
+
print_header(console, "Paper Preferences")
|
|
232
|
+
|
|
233
|
+
user = unified_user_service.get_current_user()
|
|
234
|
+
user_id = user['id']
|
|
235
|
+
|
|
236
|
+
if reset:
|
|
237
|
+
if Confirm.ask(f"{style_text('Reset all preferences to defaults?', 'warning')}", default=False):
|
|
238
|
+
# Reset preferences logic here
|
|
239
|
+
print_success(console, "Preferences reset to defaults!")
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
# Show current preferences
|
|
243
|
+
prefs_result = await unified_user_service.get_user_preferences(user_id)
|
|
244
|
+
if not prefs_result['success']:
|
|
245
|
+
print_error(console, "Failed to load preferences")
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
prefs = prefs_result['preferences']
|
|
249
|
+
await _display_preferences_overview(prefs)
|
|
250
|
+
|
|
251
|
+
# Quick actions menu
|
|
252
|
+
console.print(f"\n{style_text('Quick Actions:', 'primary')}")
|
|
253
|
+
actions = [
|
|
254
|
+
("categories", "Configure research categories"),
|
|
255
|
+
("keywords", "Manage keywords & exclusions"),
|
|
256
|
+
("daily", "Daily dose settings")
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
for cmd, desc in actions:
|
|
260
|
+
console.print(f"- {style_text(f'arionxiv settings {cmd}', 'primary')} - {desc}")
|
|
261
|
+
|
|
262
|
+
asyncio.run(_prefs())
|
|
263
|
+
|
|
264
|
+
# ================================
|
|
265
|
+
# CATEGORIES CONFIGURATION
|
|
266
|
+
# ================================
|
|
267
|
+
|
|
268
|
+
@settings.command('categories')
|
|
269
|
+
@click.option('--add', multiple=True, help='Add categories (e.g., --add cs.AI --add cs.LG)')
|
|
270
|
+
@click.option('--remove', multiple=True, help='Remove categories')
|
|
271
|
+
@click.option('--clear', is_flag=True, help='Clear all categories')
|
|
272
|
+
def categories_config(add: tuple, remove: tuple, clear: bool):
|
|
273
|
+
"""Configure research categories (ArXiv categories)"""
|
|
274
|
+
|
|
275
|
+
async def _categories():
|
|
276
|
+
await _ensure_authenticated()
|
|
277
|
+
print_header(console, "Research Categories")
|
|
278
|
+
|
|
279
|
+
user = unified_user_service.get_current_user()
|
|
280
|
+
user_id = user['id']
|
|
281
|
+
|
|
282
|
+
# Get current preferences
|
|
283
|
+
prefs_result = await unified_user_service.get_user_preferences(user_id)
|
|
284
|
+
current_categories = prefs_result['preferences']['categories'] if prefs_result['success'] else []
|
|
285
|
+
|
|
286
|
+
# Handle command-line options
|
|
287
|
+
new_categories = set(current_categories)
|
|
288
|
+
|
|
289
|
+
if clear:
|
|
290
|
+
if Confirm.ask(f"{style_text('Clear all categories?', 'warning')}", default=False):
|
|
291
|
+
new_categories.clear()
|
|
292
|
+
|
|
293
|
+
if add:
|
|
294
|
+
new_categories.update(add)
|
|
295
|
+
|
|
296
|
+
if remove:
|
|
297
|
+
new_categories.difference_update(remove)
|
|
298
|
+
|
|
299
|
+
# If any changes made via CLI, save and exit
|
|
300
|
+
if add or remove or clear:
|
|
301
|
+
await _save_categories(user_id, list(new_categories))
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
# Interactive mode
|
|
305
|
+
await _interactive_categories_config(user_id, current_categories)
|
|
306
|
+
|
|
307
|
+
asyncio.run(_categories())
|
|
308
|
+
|
|
309
|
+
# ================================
|
|
310
|
+
# KEYWORDS CONFIGURATION
|
|
311
|
+
# ================================
|
|
312
|
+
|
|
313
|
+
@settings.command('keywords')
|
|
314
|
+
@click.option('--add', multiple=True, help='Add keywords')
|
|
315
|
+
@click.option('--remove', multiple=True, help='Remove keywords')
|
|
316
|
+
@click.option('--exclude', multiple=True, help='Add exclude keywords')
|
|
317
|
+
@click.option('--clear', is_flag=True, help='Clear all keywords')
|
|
318
|
+
def keywords_config(add: tuple, remove: tuple, exclude: tuple, clear: bool):
|
|
319
|
+
"""Configure keywords and exclusions"""
|
|
320
|
+
|
|
321
|
+
async def _keywords():
|
|
322
|
+
await _ensure_authenticated()
|
|
323
|
+
print_header(console, "Keywords Configuration")
|
|
324
|
+
|
|
325
|
+
user = unified_user_service.get_current_user()
|
|
326
|
+
user_id = user['id']
|
|
327
|
+
|
|
328
|
+
# Get current preferences
|
|
329
|
+
prefs_result = await unified_user_service.get_user_preferences(user_id)
|
|
330
|
+
prefs = prefs_result['preferences'] if prefs_result['success'] else {}
|
|
331
|
+
|
|
332
|
+
current_keywords = set(prefs.get('keywords', []))
|
|
333
|
+
current_excludes = set(prefs.get('exclude_keywords', []))
|
|
334
|
+
|
|
335
|
+
# Handle CLI options
|
|
336
|
+
if clear:
|
|
337
|
+
if Confirm.ask(f"{style_text('Clear all keywords?', 'warning')}", default=False):
|
|
338
|
+
current_keywords.clear()
|
|
339
|
+
current_excludes.clear()
|
|
340
|
+
|
|
341
|
+
if add:
|
|
342
|
+
current_keywords.update(add)
|
|
343
|
+
|
|
344
|
+
if remove:
|
|
345
|
+
current_keywords.difference_update(remove)
|
|
346
|
+
|
|
347
|
+
if exclude:
|
|
348
|
+
current_excludes.update(exclude)
|
|
349
|
+
|
|
350
|
+
# If changes made via CLI, save and exit
|
|
351
|
+
if add or remove or exclude or clear:
|
|
352
|
+
await _save_keywords(user_id, list(current_keywords), list(current_excludes))
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
# Interactive mode
|
|
356
|
+
await _interactive_keywords_config(user_id, prefs)
|
|
357
|
+
|
|
358
|
+
asyncio.run(_keywords())
|
|
359
|
+
|
|
360
|
+
# ================================
|
|
361
|
+
# AUTHORS CONFIGURATION
|
|
362
|
+
# ================================
|
|
363
|
+
|
|
364
|
+
@settings.command('authors')
|
|
365
|
+
@click.option('--add', multiple=True, help='Add preferred authors')
|
|
366
|
+
@click.option('--remove', multiple=True, help='Remove authors')
|
|
367
|
+
@click.option('--clear', is_flag=True, help='Clear all authors')
|
|
368
|
+
def authors_config(add: tuple, remove: tuple, clear: bool):
|
|
369
|
+
"""Configure preferred authors"""
|
|
370
|
+
|
|
371
|
+
async def _authors():
|
|
372
|
+
await _ensure_authenticated()
|
|
373
|
+
print_header(console, "Preferred Authors")
|
|
374
|
+
|
|
375
|
+
user = unified_user_service.get_current_user()
|
|
376
|
+
user_id = user['id']
|
|
377
|
+
|
|
378
|
+
# Get current preferences
|
|
379
|
+
prefs_result = await unified_user_service.get_user_preferences(user_id)
|
|
380
|
+
current_authors = set(prefs_result['preferences']['authors']) if prefs_result['success'] else set()
|
|
381
|
+
|
|
382
|
+
# Handle CLI options
|
|
383
|
+
if clear:
|
|
384
|
+
if Confirm.ask(f"{style_text('Clear all authors?', 'warning')}", default=False):
|
|
385
|
+
current_authors.clear()
|
|
386
|
+
|
|
387
|
+
if add:
|
|
388
|
+
current_authors.update(add)
|
|
389
|
+
|
|
390
|
+
if remove:
|
|
391
|
+
current_authors.difference_update(remove)
|
|
392
|
+
|
|
393
|
+
# If changes made via CLI, save and exit
|
|
394
|
+
if add or remove or clear:
|
|
395
|
+
await _save_authors(user_id, list(current_authors))
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
# Interactive mode
|
|
399
|
+
await _interactive_authors_config(user_id, list(current_authors))
|
|
400
|
+
|
|
401
|
+
asyncio.run(_authors())
|
|
402
|
+
|
|
403
|
+
# ================================
|
|
404
|
+
# DAILY DOSE CONFIGURATION
|
|
405
|
+
# ================================
|
|
406
|
+
|
|
407
|
+
@settings.command('daily')
|
|
408
|
+
@click.option('--enable/--disable', default=None, help='Enable or disable daily dose')
|
|
409
|
+
@click.option('--time', 'time_str', help='Set delivery time in UTC (HH:MM format)')
|
|
410
|
+
@click.option('--papers', type=int, help='Max papers per day (1-10)')
|
|
411
|
+
@click.option('--keywords', help='Set keywords (comma-separated)')
|
|
412
|
+
@click.option('--show', is_flag=True, help='Show current daily dose settings')
|
|
413
|
+
def daily_config(enable: Optional[bool], time_str: Optional[str], papers: Optional[int], keywords: Optional[str], show: bool):
|
|
414
|
+
"""Configure daily dose settings via Vercel API"""
|
|
415
|
+
|
|
416
|
+
async def _daily():
|
|
417
|
+
await _ensure_authenticated()
|
|
418
|
+
|
|
419
|
+
# Use api_client for Vercel API consistency
|
|
420
|
+
from ..utils.api_client import api_client
|
|
421
|
+
|
|
422
|
+
print_header(console, "Daily Dose Configuration")
|
|
423
|
+
|
|
424
|
+
colors = get_theme_colors()
|
|
425
|
+
|
|
426
|
+
# Load current settings from Vercel API
|
|
427
|
+
settings_result = await api_client.get_settings()
|
|
428
|
+
all_settings = settings_result.get("settings", {}) if settings_result.get("success") else {}
|
|
429
|
+
current_settings = all_settings.get("daily_dose", {})
|
|
430
|
+
|
|
431
|
+
# Show current settings if requested or no changes
|
|
432
|
+
if show or (enable is None and time_str is None and papers is None and keywords is None):
|
|
433
|
+
console.print(f"\n[bold {colors['primary']}]Current Daily Dose Settings:[/bold {colors['primary']}]\n")
|
|
434
|
+
|
|
435
|
+
enabled = current_settings.get("enabled", False)
|
|
436
|
+
scheduled_time = current_settings.get("scheduled_time", "Not set")
|
|
437
|
+
max_papers = current_settings.get("max_papers", 5)
|
|
438
|
+
kw_list = current_settings.get("keywords", [])
|
|
439
|
+
|
|
440
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
441
|
+
table.add_column("Setting", style="bold white")
|
|
442
|
+
table.add_column("Value", style="white")
|
|
443
|
+
|
|
444
|
+
status_color = colors['primary'] if enabled else colors['warning']
|
|
445
|
+
table.add_row("Status", f"[bold {status_color}]{'Enabled' if enabled else 'Disabled'}[/bold {status_color}]")
|
|
446
|
+
table.add_row("Scheduled Time (UTC)", scheduled_time if scheduled_time else "[white]Not configured[/white]")
|
|
447
|
+
table.add_row("Max Papers", str(max_papers))
|
|
448
|
+
table.add_row("Keywords", ", ".join(kw_list) if kw_list else "[white]None set[/white]")
|
|
449
|
+
|
|
450
|
+
console.print(table)
|
|
451
|
+
|
|
452
|
+
if not (enable is None and time_str is None and papers is None and keywords is None):
|
|
453
|
+
return
|
|
454
|
+
|
|
455
|
+
# Interactive mode
|
|
456
|
+
console.print(f"\n[bold {colors['primary']}]Configure Daily Dose:[/bold {colors['primary']}]")
|
|
457
|
+
await _interactive_daily_dose_config_api(current_settings, all_settings, api_client)
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
# Handle CLI options
|
|
461
|
+
changes_made = False
|
|
462
|
+
update_kwargs = {}
|
|
463
|
+
|
|
464
|
+
if enable is not None:
|
|
465
|
+
update_kwargs["enabled"] = enable
|
|
466
|
+
changes_made = True
|
|
467
|
+
|
|
468
|
+
if time_str:
|
|
469
|
+
# Validate time format
|
|
470
|
+
try:
|
|
471
|
+
datetime.strptime(time_str, "%H:%M")
|
|
472
|
+
update_kwargs["scheduled_time"] = time_str
|
|
473
|
+
changes_made = True
|
|
474
|
+
except ValueError:
|
|
475
|
+
print_error(console, "Invalid time format. Use HH:MM (e.g., 09:00)")
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
if papers is not None:
|
|
479
|
+
if 1 <= papers <= 10:
|
|
480
|
+
update_kwargs["max_papers"] = papers
|
|
481
|
+
changes_made = True
|
|
482
|
+
else:
|
|
483
|
+
print_error(console, "Papers must be between 1 and 10 (v1 limit)")
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
if keywords:
|
|
487
|
+
kw_list = [k.strip() for k in keywords.split(",") if k.strip()]
|
|
488
|
+
update_kwargs["keywords"] = kw_list
|
|
489
|
+
changes_made = True
|
|
490
|
+
|
|
491
|
+
if changes_made:
|
|
492
|
+
# Merge with existing settings and update via Vercel API
|
|
493
|
+
new_daily_dose = {**current_settings, **update_kwargs}
|
|
494
|
+
all_settings["daily_dose"] = new_daily_dose
|
|
495
|
+
result = await api_client.update_settings(all_settings)
|
|
496
|
+
|
|
497
|
+
if result.get("success"):
|
|
498
|
+
print_success(console, "Daily dose settings updated successfully")
|
|
499
|
+
else:
|
|
500
|
+
print_error(console, f"Failed to update settings: {result.get('message', 'Unknown error')}")
|
|
501
|
+
|
|
502
|
+
asyncio.run(_daily())
|
|
503
|
+
|
|
504
|
+
# ================================
|
|
505
|
+
# API KEYS CONFIGURATION
|
|
506
|
+
# ================================
|
|
507
|
+
|
|
508
|
+
@settings.command('api')
|
|
509
|
+
@click.option('--show', '-s', is_flag=True, help='Show current API configuration status')
|
|
510
|
+
@click.option('--gemini', 'set_gemini', help='Set Gemini API key directly')
|
|
511
|
+
@click.option('--huggingface', '--hf', 'set_hf', help='Set HuggingFace API key directly')
|
|
512
|
+
@click.option('--groq', 'set_groq', help='Set Groq API key directly')
|
|
513
|
+
@click.option('--remove', type=click.Choice(['gemini', 'huggingface', 'groq']), help='Remove an API key')
|
|
514
|
+
def api_config(show: bool, set_gemini: Optional[str], set_hf: Optional[str], set_groq: Optional[str], remove: Optional[str]):
|
|
515
|
+
"""Configure API keys for AI services (Gemini, HuggingFace, Groq)
|
|
516
|
+
|
|
517
|
+
All API keys are FREE to obtain!
|
|
518
|
+
Keys are stored securely in ~/.arionxiv/api_keys.json
|
|
519
|
+
They persist across sessions - configure once, use forever!
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
colors = get_theme_colors()
|
|
523
|
+
print_header(console, "API Keys Configuration")
|
|
524
|
+
|
|
525
|
+
# Show info about persistence
|
|
526
|
+
console.print(f"\n[white]Your keys are stored locally and persist across sessions.[/white]")
|
|
527
|
+
console.print(f"[white]Configure once - they'll work even after logout/login![/white]\n")
|
|
528
|
+
|
|
529
|
+
# Handle direct key setting
|
|
530
|
+
if set_gemini:
|
|
531
|
+
if api_config_manager.set_api_key("gemini", set_gemini):
|
|
532
|
+
print_success(console, "Gemini API key saved successfully!")
|
|
533
|
+
else:
|
|
534
|
+
print_error(console, "Failed to save Gemini API key")
|
|
535
|
+
return
|
|
536
|
+
|
|
537
|
+
if set_hf:
|
|
538
|
+
if api_config_manager.set_api_key("huggingface", set_hf):
|
|
539
|
+
print_success(console, "HuggingFace API key saved successfully!")
|
|
540
|
+
else:
|
|
541
|
+
print_error(console, "Failed to save HuggingFace API key")
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
if set_groq:
|
|
545
|
+
if api_config_manager.set_api_key("groq", set_groq):
|
|
546
|
+
print_success(console, "Groq API key saved successfully!")
|
|
547
|
+
else:
|
|
548
|
+
print_error(console, "Failed to save Groq API key")
|
|
549
|
+
return
|
|
550
|
+
|
|
551
|
+
if remove:
|
|
552
|
+
if api_config_manager.remove_api_key(remove):
|
|
553
|
+
print_success(console, f"{remove.title()} API key removed")
|
|
554
|
+
else:
|
|
555
|
+
print_error(console, f"Failed to remove {remove} API key")
|
|
556
|
+
return
|
|
557
|
+
|
|
558
|
+
if show:
|
|
559
|
+
show_api_status(console)
|
|
560
|
+
return
|
|
561
|
+
|
|
562
|
+
# Interactive mode
|
|
563
|
+
show_api_status(console)
|
|
564
|
+
_interactive_api_config(colors)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _interactive_api_config(colors: Dict[str, str]):
|
|
568
|
+
"""Interactive API key configuration menu"""
|
|
569
|
+
|
|
570
|
+
while True:
|
|
571
|
+
# Get current status for display
|
|
572
|
+
status = api_config_manager.get_status()
|
|
573
|
+
|
|
574
|
+
gemini_status = "configured" if status["gemini"]["configured"] else "not set"
|
|
575
|
+
hf_status = "configured" if status["huggingface"]["configured"] else "not set"
|
|
576
|
+
groq_status = "configured" if status["groq"]["configured"] else "not set"
|
|
577
|
+
openrouter_status = "configured" if status["openrouter"]["configured"] else "not set"
|
|
578
|
+
openrouter_model_status = "configured" if status["openrouter_model"]["configured"] else "not set"
|
|
579
|
+
|
|
580
|
+
left_to_right_reveal(console, "\nOptions:", style=f"bold {colors['primary']}", duration=0.5)
|
|
581
|
+
left_to_right_reveal(console, f"1. Configure Gemini API key (current: {gemini_status})", style=colors['primary'], duration=0.3)
|
|
582
|
+
left_to_right_reveal(console, f"2. Configure HuggingFace API key (current: {hf_status})", style=colors['primary'], duration=0.3)
|
|
583
|
+
left_to_right_reveal(console, f"3. Configure Groq API key (current: {groq_status})", style=colors['primary'], duration=0.3)
|
|
584
|
+
left_to_right_reveal(console, f"4. Configure OpenRouter API key (current: {openrouter_status})", style=colors['primary'], duration=0.3)
|
|
585
|
+
left_to_right_reveal(console, f"5. Configure OpenRouter Model (current: {openrouter_model_status})", style=colors['primary'], duration=0.3)
|
|
586
|
+
left_to_right_reveal(console, f"6. Show API status", style=colors['primary'], duration=0.3)
|
|
587
|
+
left_to_right_reveal(console, f"7. Done - Return to main menu", style=colors['primary'], duration=0.3)
|
|
588
|
+
|
|
589
|
+
choice = Prompt.ask(
|
|
590
|
+
f"[bold {colors['primary']}]Select option[/bold {colors['primary']}]",
|
|
591
|
+
choices=["1", "2", "3", "4", "5", "6", "7"],
|
|
592
|
+
default="7"
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
if choice == "1":
|
|
596
|
+
_configure_api_key_interactive("gemini", colors)
|
|
597
|
+
elif choice == "2":
|
|
598
|
+
_configure_api_key_interactive("huggingface", colors)
|
|
599
|
+
elif choice == "3":
|
|
600
|
+
_configure_api_key_interactive("groq", colors)
|
|
601
|
+
elif choice == "4":
|
|
602
|
+
_configure_api_key_interactive("openrouter", colors)
|
|
603
|
+
elif choice == "5":
|
|
604
|
+
_configure_api_key_interactive("openrouter_model", colors)
|
|
605
|
+
elif choice == "6":
|
|
606
|
+
show_api_status(console)
|
|
607
|
+
elif choice == "7":
|
|
608
|
+
show_command_suggestions(console, context='settings')
|
|
609
|
+
break
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
# Step-by-step instructions for getting API keys
|
|
613
|
+
API_KEY_INSTRUCTIONS = {
|
|
614
|
+
"gemini": {
|
|
615
|
+
"title": "How to Get Your Google Gemini API Key (FREE)",
|
|
616
|
+
"steps": [
|
|
617
|
+
"1. Go to: https://aistudio.google.com/app/apikey",
|
|
618
|
+
"2. Sign in with your Google account",
|
|
619
|
+
"3. Click 'Create API Key'",
|
|
620
|
+
"4. Select a Google Cloud project (or create a new one)",
|
|
621
|
+
"5. Copy your API key",
|
|
622
|
+
"",
|
|
623
|
+
"Note: Gemini has a generous FREE tier - no credit card needed!"
|
|
624
|
+
]
|
|
625
|
+
},
|
|
626
|
+
"huggingface": {
|
|
627
|
+
"title": "How to Get Your HuggingFace API Token (FREE)",
|
|
628
|
+
"steps": [
|
|
629
|
+
"1. Go to: https://huggingface.co/settings/tokens",
|
|
630
|
+
"2. Create a free account or sign in",
|
|
631
|
+
"3. Click 'New token'",
|
|
632
|
+
"4. Give it a name (e.g., 'ArionXiv')",
|
|
633
|
+
"5. Select 'Read' access (that's all we need)",
|
|
634
|
+
"6. Click 'Generate token' and copy it",
|
|
635
|
+
"",
|
|
636
|
+
"Note: HuggingFace is FREE for most models!"
|
|
637
|
+
]
|
|
638
|
+
},
|
|
639
|
+
"groq": {
|
|
640
|
+
"title": "How to Get Your Groq API Key (FREE & FAST)",
|
|
641
|
+
"steps": [
|
|
642
|
+
"1. Go to: https://console.groq.com/keys",
|
|
643
|
+
"2. Create a free account or sign in",
|
|
644
|
+
"3. Click 'Create API Key'",
|
|
645
|
+
"4. Give it a name (e.g., 'ArionXiv')",
|
|
646
|
+
"5. Copy your API key",
|
|
647
|
+
"",
|
|
648
|
+
"Note: Groq is FREE and incredibly fast!",
|
|
649
|
+
" It's REQUIRED for AI analysis and chat features."
|
|
650
|
+
]
|
|
651
|
+
},
|
|
652
|
+
"openrouter": {
|
|
653
|
+
"title": "How to Get Your OpenRouter API Key (FREE Models Available)",
|
|
654
|
+
"steps": [
|
|
655
|
+
"1. Go to: https://openrouter.ai/keys",
|
|
656
|
+
"2. Create a free account or sign in",
|
|
657
|
+
"3. Click 'Create Key'",
|
|
658
|
+
"4. Give it a name (e.g., 'ArionXiv')",
|
|
659
|
+
"5. Copy your API key",
|
|
660
|
+
"",
|
|
661
|
+
"Note: OpenRouter provides access to many FREE models!",
|
|
662
|
+
" Use it for paper chat with Llama, Gemma, Qwen, etc."
|
|
663
|
+
]
|
|
664
|
+
},
|
|
665
|
+
"openrouter_model": {
|
|
666
|
+
"title": "Configure OpenRouter Model",
|
|
667
|
+
"steps": [
|
|
668
|
+
"Browse available models at: https://openrouter.ai/models",
|
|
669
|
+
"",
|
|
670
|
+
"Popular FREE models:",
|
|
671
|
+
" • meta-llama/llama-3.3-70b-instruct:free",
|
|
672
|
+
" • google/gemma-2-9b-it:free",
|
|
673
|
+
" • qwen/qwen-2.5-72b-instruct:free",
|
|
674
|
+
"",
|
|
675
|
+
"Paid models (require credits):",
|
|
676
|
+
" • openai/gpt-4o-mini",
|
|
677
|
+
" • anthropic/claude-3.5-sonnet",
|
|
678
|
+
"",
|
|
679
|
+
"Enter the full model ID as shown on OpenRouter."
|
|
680
|
+
]
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _configure_api_key_interactive(provider: str, colors: Dict[str, str]):
|
|
686
|
+
"""Configure a single API key interactively with step-by-step instructions"""
|
|
687
|
+
|
|
688
|
+
info = api_config_manager.PROVIDERS.get(provider)
|
|
689
|
+
if not info:
|
|
690
|
+
print_error(console, f"Unknown provider: {provider}")
|
|
691
|
+
return
|
|
692
|
+
|
|
693
|
+
current_key = api_config_manager.get_api_key(provider)
|
|
694
|
+
required_text = "REQUIRED" if info["required"] else "optional"
|
|
695
|
+
|
|
696
|
+
left_to_right_reveal(console, f"\n{'='*60}", style=colors['primary'], duration=0.5)
|
|
697
|
+
left_to_right_reveal(console, f"{info['name']} Configuration ({required_text})", style=f"bold {colors['primary']}", duration=0.8)
|
|
698
|
+
left_to_right_reveal(console, f"{info['description']}", style="white", duration=0.6)
|
|
699
|
+
|
|
700
|
+
# Show step-by-step instructions
|
|
701
|
+
if provider in API_KEY_INSTRUCTIONS:
|
|
702
|
+
instructions = API_KEY_INSTRUCTIONS[provider]
|
|
703
|
+
steps_text = "\n".join(instructions["steps"])
|
|
704
|
+
left_to_right_reveal(console, "", duration=0.3)
|
|
705
|
+
console.print(Panel(
|
|
706
|
+
steps_text,
|
|
707
|
+
title=f"[bold {colors['primary']}]{instructions['title']}[/bold {colors['primary']}]",
|
|
708
|
+
border_style=f"bold {colors['primary']}",
|
|
709
|
+
padding=(1, 2)
|
|
710
|
+
))
|
|
711
|
+
|
|
712
|
+
if current_key:
|
|
713
|
+
left_to_right_reveal(console, f"\nCurrently configured: {api_config_manager._mask_key(current_key)}", style=colors['primary'], duration=0.8)
|
|
714
|
+
|
|
715
|
+
action = Prompt.ask(
|
|
716
|
+
f"\n[bold {colors['primary']}]What would you like to do?[/bold {colors['primary']}]",
|
|
717
|
+
choices=["update", "remove", "cancel"],
|
|
718
|
+
default="cancel"
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
if action == "update":
|
|
722
|
+
new_key = Prompt.ask(f"[bold {colors['primary']}]Enter new API key[/bold {colors['primary']}]", default="", show_default=False)
|
|
723
|
+
if new_key.strip():
|
|
724
|
+
if api_config_manager.set_api_key(provider, new_key.strip()):
|
|
725
|
+
print_success(console, f"{info['name']} key updated successfully!")
|
|
726
|
+
else:
|
|
727
|
+
print_error(console, "Failed to save key")
|
|
728
|
+
else:
|
|
729
|
+
print_warning(console, "No key entered, keeping existing")
|
|
730
|
+
elif action == "remove":
|
|
731
|
+
if Confirm.ask(f"[bold {colors['warning']}]Remove {info['name']} key?[/bold {colors['warning']}]", default=False):
|
|
732
|
+
if api_config_manager.remove_api_key(provider):
|
|
733
|
+
print_success(console, f"{info['name']} key removed")
|
|
734
|
+
else:
|
|
735
|
+
print_error(console, "Failed to remove key")
|
|
736
|
+
else:
|
|
737
|
+
new_key = Prompt.ask(
|
|
738
|
+
f"\n[bold {colors['primary']}]Enter {info['name']} API key (or press Enter to skip)[/bold {colors['primary']}]",
|
|
739
|
+
default="",
|
|
740
|
+
show_default=False
|
|
741
|
+
)
|
|
742
|
+
if new_key.strip():
|
|
743
|
+
if api_config_manager.set_api_key(provider, new_key.strip()):
|
|
744
|
+
print_success(console, f"{info['name']} key saved successfully!")
|
|
745
|
+
else:
|
|
746
|
+
print_error(console, "Failed to save key")
|
|
747
|
+
else:
|
|
748
|
+
if info["required"]:
|
|
749
|
+
print_warning(console, f"{info['name']} key is REQUIRED for AI features")
|
|
750
|
+
else:
|
|
751
|
+
left_to_right_reveal(console, f"Skipped {info['name']}", style="white", duration=0.8)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
# ================================
|
|
755
|
+
# TIME CONFIGURATION (Quick access)
|
|
756
|
+
# ================================
|
|
757
|
+
|
|
758
|
+
@settings.command('time')
|
|
759
|
+
@click.argument('time_str', required=False)
|
|
760
|
+
def time_config(time_str: Optional[str]):
|
|
761
|
+
"""Set daily analysis delivery time (HH:MM format)"""
|
|
762
|
+
|
|
763
|
+
async def _time():
|
|
764
|
+
await _ensure_authenticated()
|
|
765
|
+
|
|
766
|
+
if time_str:
|
|
767
|
+
# Validate time format
|
|
768
|
+
try:
|
|
769
|
+
datetime.strptime(time_str, "%H:%M")
|
|
770
|
+
# Save time setting
|
|
771
|
+
print_success(console, f"Daily analysis time set to {time_str}")
|
|
772
|
+
print_warning(console, "Use 'arionxiv settings daily' for more options")
|
|
773
|
+
except ValueError:
|
|
774
|
+
print_error(console, "Invalid time format. Use HH:MM (e.g., 09:00)")
|
|
775
|
+
else:
|
|
776
|
+
# Show current time and prompt for new one
|
|
777
|
+
print_header(console, "Daily Analysis Time")
|
|
778
|
+
console.print("Current time: [bold]09:00[/bold] (example)")
|
|
779
|
+
|
|
780
|
+
new_time = Prompt.ask(
|
|
781
|
+
f"{style_text('Enter new time (HH:MM)', 'primary')}",
|
|
782
|
+
default="09:00"
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
try:
|
|
786
|
+
datetime.strptime(new_time, "%H:%M")
|
|
787
|
+
print_success(console, f"Daily analysis time set to {new_time}")
|
|
788
|
+
except ValueError:
|
|
789
|
+
print_error(console, "Invalid time format")
|
|
790
|
+
|
|
791
|
+
asyncio.run(_time())
|
|
792
|
+
|
|
793
|
+
# ================================
|
|
794
|
+
# SAVED PAPERS MANAGEMENT
|
|
795
|
+
# ================================
|
|
796
|
+
|
|
797
|
+
@settings.command('papers')
|
|
798
|
+
def papers_config():
|
|
799
|
+
"""Manage your saved papers (delete papers from library)"""
|
|
800
|
+
|
|
801
|
+
async def _papers():
|
|
802
|
+
await _ensure_authenticated()
|
|
803
|
+
print_header(console, "Saved Papers Management")
|
|
804
|
+
|
|
805
|
+
colors = get_theme_colors()
|
|
806
|
+
|
|
807
|
+
# Get user's saved papers from API
|
|
808
|
+
try:
|
|
809
|
+
result = await api_client.get_library(limit=100)
|
|
810
|
+
if not result.get("success"):
|
|
811
|
+
print_error(console, "Failed to fetch library")
|
|
812
|
+
return
|
|
813
|
+
user_papers = result.get("papers", [])
|
|
814
|
+
except APIClientError as e:
|
|
815
|
+
print_error(console, f"API Error: {e.message}")
|
|
816
|
+
return
|
|
817
|
+
|
|
818
|
+
if not user_papers:
|
|
819
|
+
print_warning(console, "No saved papers in your library.")
|
|
820
|
+
console.print(f"\n{style_text('Use arionxiv chat to chat with papers and save them.', 'primary')}")
|
|
821
|
+
show_command_suggestions(console, context='settings')
|
|
822
|
+
return
|
|
823
|
+
|
|
824
|
+
console.print(f"\n[bold {colors['primary']}]Your saved papers ({len(user_papers)}/10):[/bold {colors['primary']}]\n")
|
|
825
|
+
|
|
826
|
+
# Create table
|
|
827
|
+
table = Table(show_header=True, header_style=f"bold {colors['primary']}")
|
|
828
|
+
table.add_column("#", style="bold white", width=3)
|
|
829
|
+
table.add_column("Title", style="white", max_width=50)
|
|
830
|
+
table.add_column("ArXiv ID", style="white", width=15)
|
|
831
|
+
table.add_column("Added", style="white", width=12)
|
|
832
|
+
|
|
833
|
+
for i, paper in enumerate(user_papers):
|
|
834
|
+
title = paper.get("title", "Unknown")
|
|
835
|
+
|
|
836
|
+
arxiv_id = paper.get("arxiv_id", "Unknown")
|
|
837
|
+
added_at = paper.get("added_at")
|
|
838
|
+
if added_at:
|
|
839
|
+
added_str = added_at.strftime("%Y-%m-%d") if hasattr(added_at, 'strftime') else str(added_at)[:10]
|
|
840
|
+
else:
|
|
841
|
+
added_str = "Unknown"
|
|
842
|
+
table.add_row(str(i + 1), title, arxiv_id, added_str)
|
|
843
|
+
|
|
844
|
+
console.print(table)
|
|
845
|
+
|
|
846
|
+
# Options
|
|
847
|
+
console.print(f"\n{style_text('Actions:', 'primary')}")
|
|
848
|
+
console.print(f"[bold {colors['primary']}]1.[/bold {colors['primary']}] Delete papers")
|
|
849
|
+
console.print(f"[bold {colors['primary']}]2.[/bold {colors['primary']}] Exit")
|
|
850
|
+
|
|
851
|
+
action = Prompt.ask(f"[bold {colors['primary']}]Select action[/bold {colors['primary']}]", choices=["1", "2"], default="2")
|
|
852
|
+
|
|
853
|
+
if action == "1":
|
|
854
|
+
console.print(f"\n[bold {colors['primary']}]Enter paper numbers to delete (comma-separated, e.g., 1,3,5) or 0 to cancel:[/bold {colors['primary']}]")
|
|
855
|
+
|
|
856
|
+
choice = Prompt.ask(f"[bold {colors['primary']}]Papers to delete[/bold {colors['primary']}]")
|
|
857
|
+
|
|
858
|
+
if choice.strip() == "0" or not choice.strip():
|
|
859
|
+
print_warning(console, "Cancelled.")
|
|
860
|
+
return
|
|
861
|
+
|
|
862
|
+
try:
|
|
863
|
+
indices = [int(x.strip()) - 1 for x in choice.split(",")]
|
|
864
|
+
valid_indices = [i for i in indices if 0 <= i < len(user_papers)]
|
|
865
|
+
|
|
866
|
+
if not valid_indices:
|
|
867
|
+
print_error(console, "No valid selections.")
|
|
868
|
+
return
|
|
869
|
+
|
|
870
|
+
# Confirm deletion
|
|
871
|
+
papers_to_delete = [user_papers[i].get("title", "Unknown")[:30] for i in valid_indices]
|
|
872
|
+
console.print(f"\n[bold {colors['primary']}]Papers to delete:[/bold {colors['primary']}]")
|
|
873
|
+
for title in papers_to_delete:
|
|
874
|
+
console.print(f" - {title}")
|
|
875
|
+
|
|
876
|
+
if Confirm.ask(f"\n[bold {colors['red']}]Confirm deletion?[/bold {colors['red']}]", default=False):
|
|
877
|
+
deleted_count = 0
|
|
878
|
+
for idx in valid_indices:
|
|
879
|
+
paper = user_papers[idx]
|
|
880
|
+
arxiv_id = paper.get("arxiv_id")
|
|
881
|
+
if arxiv_id:
|
|
882
|
+
try:
|
|
883
|
+
result = await api_client.remove_from_library(arxiv_id)
|
|
884
|
+
if result.get("success"):
|
|
885
|
+
deleted_count += 1
|
|
886
|
+
except APIClientError as e:
|
|
887
|
+
logging.error(f"Failed to remove paper from library: {e}", exc_info=True)
|
|
888
|
+
console.print(f"[bold {colors['error']}]Failed to remove paper from library.[/bold {colors['error']}")
|
|
889
|
+
|
|
890
|
+
print_success(console, f"Deleted {deleted_count} paper(s) from your library.")
|
|
891
|
+
else:
|
|
892
|
+
print_warning(console, "Deletion cancelled.")
|
|
893
|
+
|
|
894
|
+
except ValueError:
|
|
895
|
+
print_error(console, "Invalid input. Use comma-separated numbers.")
|
|
896
|
+
|
|
897
|
+
# Show command suggestions
|
|
898
|
+
show_command_suggestions(console, context='settings')
|
|
899
|
+
|
|
900
|
+
asyncio.run(_papers())
|
|
901
|
+
|
|
902
|
+
# ================================
|
|
903
|
+
# HELPER FUNCTIONS
|
|
904
|
+
# ================================
|
|
905
|
+
|
|
906
|
+
async def _ensure_authenticated():
|
|
907
|
+
"""Ensure user is authenticated"""
|
|
908
|
+
if not unified_user_service.is_authenticated():
|
|
909
|
+
print_error(console, "Please login first: arionxiv auth --login")
|
|
910
|
+
raise click.Abort()
|
|
911
|
+
|
|
912
|
+
async def _show_compact_settings(prefs: Dict[str, Any], theme_color: str):
|
|
913
|
+
"""Show compact settings overview"""
|
|
914
|
+
colors = get_theme_colors()
|
|
915
|
+
|
|
916
|
+
# Create a summary table
|
|
917
|
+
table = Table(title="Settings Summary", show_header=True, header_style=f"bold {colors['primary']}")
|
|
918
|
+
table.add_column("Setting", style="bold white", width=20)
|
|
919
|
+
table.add_column("Value", style="white", width=40)
|
|
920
|
+
table.add_column("Command", style="white", width=25)
|
|
921
|
+
|
|
922
|
+
# Theme
|
|
923
|
+
table.add_row("Theme Color", f"[{theme_color}]{theme_color.title()}[/{theme_color}]", "settings theme")
|
|
924
|
+
|
|
925
|
+
# Categories
|
|
926
|
+
categories = prefs.get('categories', [])
|
|
927
|
+
table.add_row("Categories", ", ".join(categories[:3]) + ("..." if len(categories) > 3 else ""), "settings categories")
|
|
928
|
+
|
|
929
|
+
# Keywords
|
|
930
|
+
keywords = prefs.get('keywords', [])
|
|
931
|
+
table.add_row("Keywords", ", ".join(keywords[:3]) + ("..." if len(keywords) > 3 else ""), "settings keywords")
|
|
932
|
+
|
|
933
|
+
# Authors
|
|
934
|
+
authors = prefs.get('authors', [])
|
|
935
|
+
table.add_row("Authors", ", ".join(authors[:2]) + ("..." if len(authors) > 2 else ""), "settings authors")
|
|
936
|
+
|
|
937
|
+
# Daily settings
|
|
938
|
+
max_papers = prefs.get('max_papers_per_day', 10)
|
|
939
|
+
table.add_row("Max Papers/Day", str(max_papers), "settings daily")
|
|
940
|
+
|
|
941
|
+
# Relevance threshold
|
|
942
|
+
min_relevance = prefs.get('min_relevance_score', 0.2)
|
|
943
|
+
table.add_row("Min Relevance", f"{min_relevance:.1f}", "settings prefs")
|
|
944
|
+
|
|
945
|
+
console.print(table)
|
|
946
|
+
|
|
947
|
+
async def _show_detailed_settings(prefs: Dict[str, Any], theme_color: str):
|
|
948
|
+
"""Show detailed settings view"""
|
|
949
|
+
|
|
950
|
+
# Theme section
|
|
951
|
+
theme_panel = Panel(
|
|
952
|
+
f"Color Theme: [bold {theme_color}]{theme_color.title()}[/bold {theme_color}]\n"
|
|
953
|
+
f"Use: [white]arionxiv settings theme[/white]",
|
|
954
|
+
title="Display",
|
|
955
|
+
border_style=theme_color
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
# Preferences section
|
|
959
|
+
categories = prefs.get('categories', [])
|
|
960
|
+
keywords = prefs.get('keywords', [])
|
|
961
|
+
authors = prefs.get('authors', [])
|
|
962
|
+
exclude_keywords = prefs.get('exclude_keywords', [])
|
|
963
|
+
|
|
964
|
+
prefs_content = f"""
|
|
965
|
+
Categories: {', '.join(categories) if categories else 'None set'}
|
|
966
|
+
Keywords: {', '.join(keywords) if keywords else 'None set'}
|
|
967
|
+
Authors: {', '.join(authors) if authors else 'None set'}
|
|
968
|
+
Exclude: {', '.join(exclude_keywords) if exclude_keywords else 'None set'}
|
|
969
|
+
|
|
970
|
+
Max Papers/Day: {prefs.get('max_papers_per_day', 10)}
|
|
971
|
+
Min Relevance: {prefs.get('min_relevance_score', 0.2):.1f}
|
|
972
|
+
""".strip()
|
|
973
|
+
|
|
974
|
+
prefs_panel = Panel(
|
|
975
|
+
prefs_content,
|
|
976
|
+
title="Paper Preferences",
|
|
977
|
+
border_style=theme_color
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
# Daily dose section
|
|
981
|
+
daily_content = """
|
|
982
|
+
Status: Enabled
|
|
983
|
+
Delivery Time: 09:00
|
|
984
|
+
Fetch Days: 1 day back
|
|
985
|
+
Analysis: LLM-powered
|
|
986
|
+
""".strip()
|
|
987
|
+
|
|
988
|
+
daily_panel = Panel(
|
|
989
|
+
daily_content,
|
|
990
|
+
title="Daily Dose",
|
|
991
|
+
border_style=theme_color
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
# Display panels
|
|
995
|
+
console.print(Columns([theme_panel, prefs_panel]))
|
|
996
|
+
console.print(daily_panel)
|
|
997
|
+
|
|
998
|
+
async def _display_preferences_overview(prefs: Dict[str, Any]):
|
|
999
|
+
"""Display preferences in a clean overview format"""
|
|
1000
|
+
|
|
1001
|
+
colors = get_theme_colors()
|
|
1002
|
+
|
|
1003
|
+
# Categories
|
|
1004
|
+
categories = prefs.get('categories', [])
|
|
1005
|
+
if categories:
|
|
1006
|
+
console.print(f"\n{style_text('Research Categories:', 'primary')}")
|
|
1007
|
+
for cat in categories:
|
|
1008
|
+
console.print(f" • {cat}")
|
|
1009
|
+
else:
|
|
1010
|
+
console.print(f"\n{style_text('Research Categories:', 'primary')} None set")
|
|
1011
|
+
|
|
1012
|
+
# Keywords
|
|
1013
|
+
keywords = prefs.get('keywords', [])
|
|
1014
|
+
if keywords:
|
|
1015
|
+
console.print(f"\n{style_text('Keywords:', 'primary')}")
|
|
1016
|
+
for kw in keywords:
|
|
1017
|
+
console.print(f" - {kw}")
|
|
1018
|
+
else:
|
|
1019
|
+
console.print(f"\n{style_text('Keywords:', 'primary')} None set")
|
|
1020
|
+
|
|
1021
|
+
# Authors
|
|
1022
|
+
authors = prefs.get('authors', [])
|
|
1023
|
+
if authors:
|
|
1024
|
+
console.print(f"\n{style_text('Preferred Authors:', 'primary')}")
|
|
1025
|
+
for author in authors:
|
|
1026
|
+
console.print(f" • {author}")
|
|
1027
|
+
else:
|
|
1028
|
+
console.print(f"\n{style_text('Preferred Authors:', 'primary')} None set")
|
|
1029
|
+
|
|
1030
|
+
# Settings
|
|
1031
|
+
console.print(f"\n{style_text('Settings:', 'primary')}")
|
|
1032
|
+
console.print(f" • Max papers per day: {prefs.get('max_papers_per_day', 10)}")
|
|
1033
|
+
console.print(f" • Min relevance score: {prefs.get('min_relevance_score', 0.2):.1f}")
|
|
1034
|
+
|
|
1035
|
+
async def _interactive_categories_config(user_id: str, current_categories: List[str]):
|
|
1036
|
+
"""Interactive categories configuration"""
|
|
1037
|
+
|
|
1038
|
+
# Show available categories
|
|
1039
|
+
available_cats = unified_user_service.get_available_categories()
|
|
1040
|
+
|
|
1041
|
+
console.print(f"\n{style_text('Available ArXiv Categories:', 'primary')}")
|
|
1042
|
+
|
|
1043
|
+
colors = get_theme_colors()
|
|
1044
|
+
|
|
1045
|
+
# Create table of categories
|
|
1046
|
+
table = Table(show_header=True, header_style=f"bold {colors['primary']}")
|
|
1047
|
+
table.add_column("Code", style="bold", width=8)
|
|
1048
|
+
table.add_column("Description", width=40)
|
|
1049
|
+
table.add_column("Selected", width=8)
|
|
1050
|
+
|
|
1051
|
+
for code, desc in list(available_cats.items())[:10]: # Show top 10
|
|
1052
|
+
selected = "[X]" if code in current_categories else "[ ]"
|
|
1053
|
+
table.add_row(code, desc, selected)
|
|
1054
|
+
|
|
1055
|
+
console.print(table)
|
|
1056
|
+
console.print(f"\n{style_text('Current selections:', 'primary')} {', '.join(current_categories)}")
|
|
1057
|
+
|
|
1058
|
+
# Interactive selection
|
|
1059
|
+
action = Prompt.ask(
|
|
1060
|
+
"\n[bold]Action[/bold]",
|
|
1061
|
+
choices=["add", "remove", "clear", "done"],
|
|
1062
|
+
default="done"
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
if action == "add":
|
|
1066
|
+
new_cat = Prompt.ask("Enter category code (e.g., cs.AI)")
|
|
1067
|
+
if new_cat in available_cats:
|
|
1068
|
+
new_categories = list(set(current_categories + [new_cat]))
|
|
1069
|
+
await _save_categories(user_id, new_categories)
|
|
1070
|
+
else:
|
|
1071
|
+
print_error(console, "Invalid category code")
|
|
1072
|
+
|
|
1073
|
+
elif action == "remove":
|
|
1074
|
+
if current_categories:
|
|
1075
|
+
cat_to_remove = Prompt.ask("Enter category to remove", choices=current_categories)
|
|
1076
|
+
new_categories = [c for c in current_categories if c != cat_to_remove]
|
|
1077
|
+
await _save_categories(user_id, new_categories)
|
|
1078
|
+
else:
|
|
1079
|
+
print_warning(console, "No categories to remove")
|
|
1080
|
+
|
|
1081
|
+
elif action == "clear":
|
|
1082
|
+
if Confirm.ask("Clear all categories?", default=False):
|
|
1083
|
+
await _save_categories(user_id, [])
|
|
1084
|
+
|
|
1085
|
+
async def _interactive_keywords_config(user_id: str, prefs: Dict[str, Any]):
|
|
1086
|
+
"""Interactive keywords configuration"""
|
|
1087
|
+
|
|
1088
|
+
keywords = prefs.get('keywords', [])
|
|
1089
|
+
exclude_keywords = prefs.get('exclude_keywords', [])
|
|
1090
|
+
|
|
1091
|
+
console.print(f"\n{style_text('Current Keywords:', 'primary')}")
|
|
1092
|
+
console.print(f"Include: {', '.join(keywords) if keywords else 'None'}")
|
|
1093
|
+
console.print(f"Exclude: {', '.join(exclude_keywords) if exclude_keywords else 'None'}")
|
|
1094
|
+
|
|
1095
|
+
action = Prompt.ask(
|
|
1096
|
+
"\n[bold]Action[/bold]",
|
|
1097
|
+
choices=["add", "exclude", "remove", "clear", "done"],
|
|
1098
|
+
default="done"
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
if action == "add":
|
|
1102
|
+
new_keywords = Prompt.ask("Enter keywords (comma-separated)")
|
|
1103
|
+
new_kw_list = [kw.strip() for kw in new_keywords.split(',') if kw.strip()]
|
|
1104
|
+
combined = list(set(keywords + new_kw_list))
|
|
1105
|
+
await _save_keywords(user_id, combined, exclude_keywords)
|
|
1106
|
+
|
|
1107
|
+
elif action == "exclude":
|
|
1108
|
+
exclude_kw = Prompt.ask("Enter keywords to exclude (comma-separated)")
|
|
1109
|
+
new_exclude = [kw.strip() for kw in exclude_kw.split(',') if kw.strip()]
|
|
1110
|
+
combined_exclude = list(set(exclude_keywords + new_exclude))
|
|
1111
|
+
await _save_keywords(user_id, keywords, combined_exclude)
|
|
1112
|
+
|
|
1113
|
+
elif action == "remove":
|
|
1114
|
+
if keywords:
|
|
1115
|
+
kw_to_remove = Prompt.ask("Enter keyword to remove", choices=keywords)
|
|
1116
|
+
new_keywords = [k for k in keywords if k != kw_to_remove]
|
|
1117
|
+
await _save_keywords(user_id, new_keywords, exclude_keywords)
|
|
1118
|
+
|
|
1119
|
+
elif action == "clear":
|
|
1120
|
+
if Confirm.ask("Clear all keywords?", default=False):
|
|
1121
|
+
await _save_keywords(user_id, [], [])
|
|
1122
|
+
|
|
1123
|
+
async def _interactive_authors_config(user_id: str, current_authors: List[str]):
|
|
1124
|
+
"""Interactive authors configuration"""
|
|
1125
|
+
|
|
1126
|
+
console.print(f"\n{style_text('Current Authors:', 'primary')}")
|
|
1127
|
+
if current_authors:
|
|
1128
|
+
for author in current_authors:
|
|
1129
|
+
console.print(f" • {author}")
|
|
1130
|
+
else:
|
|
1131
|
+
console.print(" None set")
|
|
1132
|
+
|
|
1133
|
+
action = Prompt.ask(
|
|
1134
|
+
"\n[bold]Action[/bold]",
|
|
1135
|
+
choices=["add", "remove", "clear", "done"],
|
|
1136
|
+
default="done"
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
if action == "add":
|
|
1140
|
+
new_author = Prompt.ask("Enter author name")
|
|
1141
|
+
new_authors = list(set(current_authors + [new_author.strip()]))
|
|
1142
|
+
await _save_authors(user_id, new_authors)
|
|
1143
|
+
|
|
1144
|
+
elif action == "remove":
|
|
1145
|
+
if current_authors:
|
|
1146
|
+
author_to_remove = Prompt.ask("Enter author to remove", choices=current_authors)
|
|
1147
|
+
new_authors = [a for a in current_authors if a != author_to_remove]
|
|
1148
|
+
await _save_authors(user_id, new_authors)
|
|
1149
|
+
|
|
1150
|
+
elif action == "clear":
|
|
1151
|
+
if Confirm.ask("Clear all authors?", default=False):
|
|
1152
|
+
await _save_authors(user_id, [])
|
|
1153
|
+
|
|
1154
|
+
async def _interactive_daily_config(user_id: str, prefs: Dict[str, Any]):
|
|
1155
|
+
"""Interactive daily dose configuration"""
|
|
1156
|
+
|
|
1157
|
+
console.print(f"\n{style_text('Daily Dose Settings:', 'primary')}")
|
|
1158
|
+
|
|
1159
|
+
# Current settings
|
|
1160
|
+
max_papers = prefs.get('max_papers_per_day', 10)
|
|
1161
|
+
min_relevance = prefs.get('min_relevance_score', 0.2)
|
|
1162
|
+
|
|
1163
|
+
console.print(f"Max papers per day: {max_papers}")
|
|
1164
|
+
console.print(f"Min relevance score: {min_relevance:.1f}")
|
|
1165
|
+
|
|
1166
|
+
# Configuration options
|
|
1167
|
+
if Confirm.ask("\nChange max papers per day?", default=False):
|
|
1168
|
+
new_max = IntPrompt.ask("Enter max papers (1-50)", default=max_papers)
|
|
1169
|
+
if 1 <= new_max <= 50:
|
|
1170
|
+
prefs['max_papers_per_day'] = new_max
|
|
1171
|
+
await unified_user_service.update_user_preferences(user_id, prefs)
|
|
1172
|
+
print_success(console, f"Max papers updated to {new_max}")
|
|
1173
|
+
|
|
1174
|
+
if Confirm.ask("Change relevance threshold?", default=False):
|
|
1175
|
+
new_relevance = float(Prompt.ask("Enter min relevance (0.0-1.0)", default=str(min_relevance)))
|
|
1176
|
+
if 0.0 <= new_relevance <= 1.0:
|
|
1177
|
+
prefs['min_relevance_score'] = new_relevance
|
|
1178
|
+
await unified_user_service.update_user_preferences(user_id, prefs)
|
|
1179
|
+
print_success(console, f"Min relevance updated to {new_relevance:.1f}")
|
|
1180
|
+
|
|
1181
|
+
async def _interactive_daily_dose_config(user_id: str, current_settings: Dict[str, Any], daily_dose_service):
|
|
1182
|
+
"""Interactive daily dose configuration with full options"""
|
|
1183
|
+
|
|
1184
|
+
colors = get_theme_colors()
|
|
1185
|
+
|
|
1186
|
+
while True:
|
|
1187
|
+
# Get current values for display
|
|
1188
|
+
enabled = current_settings.get("enabled", False)
|
|
1189
|
+
scheduled_time = current_settings.get("scheduled_time", "08:00") or "08:00"
|
|
1190
|
+
max_papers = current_settings.get("max_papers", 5)
|
|
1191
|
+
keywords = current_settings.get("keywords", [])
|
|
1192
|
+
keywords_str = ", ".join(keywords[:3]) + ("..." if len(keywords) > 3 else "") if keywords else "None"
|
|
1193
|
+
|
|
1194
|
+
console.print(f"\n[bold {colors['primary']}]Options:[/bold {colors['primary']}]")
|
|
1195
|
+
console.print(f"[bold {colors['primary']}]1.[/bold {colors['primary']}] Toggle enabled/disabled [white](current: {'Enabled' if enabled else 'Disabled'})[/white]")
|
|
1196
|
+
console.print(f"[bold {colors['primary']}]2.[/bold {colors['primary']}] Set scheduled time (UTC) [white](current: {scheduled_time})[/white]")
|
|
1197
|
+
console.print(f"[bold {colors['primary']}]3.[/bold {colors['primary']}] Set max papers (1-10) [white](current: {max_papers})[/white]")
|
|
1198
|
+
console.print(f"[bold {colors['primary']}]4.[/bold {colors['primary']}] Set keywords [white](current: {keywords_str})[/white]")
|
|
1199
|
+
console.print(f"[bold {colors['primary']}]5.[/bold {colors['primary']}] Done - Return to main menu")
|
|
1200
|
+
|
|
1201
|
+
choice = Prompt.ask(f"[bold {colors['primary']}]Select option[/bold {colors['primary']}]", choices=["1", "2", "3", "4", "5"], default="5")
|
|
1202
|
+
|
|
1203
|
+
if choice == "1":
|
|
1204
|
+
current_enabled = current_settings.get("enabled", False)
|
|
1205
|
+
new_enabled = not current_enabled
|
|
1206
|
+
result = await daily_dose_service.update_user_daily_dose_settings(user_id, enabled=new_enabled)
|
|
1207
|
+
if result["success"]:
|
|
1208
|
+
current_settings["enabled"] = new_enabled
|
|
1209
|
+
status = "enabled" if new_enabled else "disabled"
|
|
1210
|
+
print_success(console, f"Daily dose {status}")
|
|
1211
|
+
else:
|
|
1212
|
+
print_error(console, f"Failed to update: {result.get('message')}")
|
|
1213
|
+
|
|
1214
|
+
elif choice == "2":
|
|
1215
|
+
current_time = current_settings.get("scheduled_time", "08:00")
|
|
1216
|
+
console.print(f"[white]Note: Time is in UTC timezone. Your daily dose will run at this UTC time.[/white]")
|
|
1217
|
+
new_time = Prompt.ask(f"[bold {colors['primary']}]Enter time in UTC (HH:MM)[/bold {colors['primary']}]", default=current_time or "08:00")
|
|
1218
|
+
try:
|
|
1219
|
+
datetime.strptime(new_time, "%H:%M")
|
|
1220
|
+
result = await daily_dose_service.update_user_daily_dose_settings(user_id, scheduled_time=new_time)
|
|
1221
|
+
if result["success"]:
|
|
1222
|
+
current_settings["scheduled_time"] = new_time
|
|
1223
|
+
print_success(console, f"Scheduled time set to {new_time} UTC")
|
|
1224
|
+
|
|
1225
|
+
# Schedule the job if enabled
|
|
1226
|
+
if current_settings.get("enabled") and schedule_user_daily_dose:
|
|
1227
|
+
schedule_result = await schedule_user_daily_dose(user_id, new_time)
|
|
1228
|
+
if schedule_result.get("success"):
|
|
1229
|
+
print_success(console, "Cron job scheduled")
|
|
1230
|
+
else:
|
|
1231
|
+
print_error(console, f"Failed to update: {result.get('message')}")
|
|
1232
|
+
except ValueError:
|
|
1233
|
+
print_error(console, "Invalid time format. Use HH:MM")
|
|
1234
|
+
|
|
1235
|
+
elif choice == "3":
|
|
1236
|
+
current_max = current_settings.get("max_papers", 5)
|
|
1237
|
+
new_max = IntPrompt.ask(f"[bold {colors['primary']}]Enter max papers (1-10)[/bold {colors['primary']}]", default=current_max)
|
|
1238
|
+
if 1 <= new_max <= 10:
|
|
1239
|
+
result = await daily_dose_service.update_user_daily_dose_settings(user_id, max_papers=new_max)
|
|
1240
|
+
if result["success"]:
|
|
1241
|
+
current_settings["max_papers"] = new_max
|
|
1242
|
+
print_success(console, f"Max papers set to {new_max}")
|
|
1243
|
+
else:
|
|
1244
|
+
print_error(console, f"Failed to update: {result.get('message')}")
|
|
1245
|
+
else:
|
|
1246
|
+
print_error(console, "Max papers must be between 1 and 10")
|
|
1247
|
+
|
|
1248
|
+
elif choice == "4":
|
|
1249
|
+
current_keywords = current_settings.get("keywords", [])
|
|
1250
|
+
console.print(f"\n[bold {colors['primary']}]Current keywords:[/bold {colors['primary']}] {', '.join(current_keywords) if current_keywords else 'None'}")
|
|
1251
|
+
new_keywords_str = Prompt.ask(f"[bold {colors['primary']}]Enter keywords (comma-separated)[/bold {colors['primary']}]", default=", ".join(current_keywords))
|
|
1252
|
+
new_keywords = [k.strip() for k in new_keywords_str.split(",") if k.strip()]
|
|
1253
|
+
result = await daily_dose_service.update_user_daily_dose_settings(user_id, keywords=new_keywords)
|
|
1254
|
+
if result["success"]:
|
|
1255
|
+
current_settings["keywords"] = new_keywords
|
|
1256
|
+
print_success(console, f"Keywords updated: {', '.join(new_keywords)}")
|
|
1257
|
+
else:
|
|
1258
|
+
print_error(console, f"Failed to update: {result.get('message')}")
|
|
1259
|
+
|
|
1260
|
+
elif choice == "5":
|
|
1261
|
+
# Show main menu / command suggestions before exiting
|
|
1262
|
+
show_command_suggestions(console, context='settings')
|
|
1263
|
+
break
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
async def _interactive_daily_dose_config_api(current_settings: Dict[str, Any], all_settings: Dict[str, Any], api_client):
|
|
1267
|
+
"""Interactive daily dose configuration using Vercel API"""
|
|
1268
|
+
|
|
1269
|
+
colors = get_theme_colors()
|
|
1270
|
+
|
|
1271
|
+
while True:
|
|
1272
|
+
# Get current values for display
|
|
1273
|
+
enabled = current_settings.get("enabled", False)
|
|
1274
|
+
scheduled_time = current_settings.get("scheduled_time", "08:00") or "08:00"
|
|
1275
|
+
max_papers = current_settings.get("max_papers", 5)
|
|
1276
|
+
keywords = current_settings.get("keywords", [])
|
|
1277
|
+
keywords_str = ", ".join(keywords[:3]) + ("..." if len(keywords) > 3 else "") if keywords else "None"
|
|
1278
|
+
|
|
1279
|
+
console.print(f"\n[bold {colors['primary']}]Options:[/bold {colors['primary']}]")
|
|
1280
|
+
console.print(f"[bold {colors['primary']}]1.[/bold {colors['primary']}] Toggle enabled/disabled [white](current: {'Enabled' if enabled else 'Disabled'})[/white]")
|
|
1281
|
+
console.print(f"[bold {colors['primary']}]2.[/bold {colors['primary']}] Set scheduled time (UTC) [white](current: {scheduled_time})[/white]")
|
|
1282
|
+
console.print(f"[bold {colors['primary']}]3.[/bold {colors['primary']}] Set max papers (1-10) [white](current: {max_papers})[/white]")
|
|
1283
|
+
console.print(f"[bold {colors['primary']}]4.[/bold {colors['primary']}] Set keywords [white](current: {keywords_str})[/white]")
|
|
1284
|
+
console.print(f"[bold {colors['primary']}]5.[/bold {colors['primary']}] Done - Return to main menu")
|
|
1285
|
+
|
|
1286
|
+
choice = Prompt.ask(f"[bold {colors['primary']}]Select option[/bold {colors['primary']}]", choices=["1", "2", "3", "4", "5"], default="5")
|
|
1287
|
+
|
|
1288
|
+
if choice == "1":
|
|
1289
|
+
new_enabled = not enabled
|
|
1290
|
+
current_settings["enabled"] = new_enabled
|
|
1291
|
+
all_settings["daily_dose"] = current_settings
|
|
1292
|
+
result = await api_client.update_settings(all_settings)
|
|
1293
|
+
if result.get("success"):
|
|
1294
|
+
status = "enabled" if new_enabled else "disabled"
|
|
1295
|
+
print_success(console, f"Daily dose {status}")
|
|
1296
|
+
else:
|
|
1297
|
+
current_settings["enabled"] = enabled # Revert on failure
|
|
1298
|
+
print_error(console, f"Failed to update: {result.get('message', 'Unknown error')}")
|
|
1299
|
+
|
|
1300
|
+
elif choice == "2":
|
|
1301
|
+
console.print(f"[white]Note: Time is in UTC timezone. Your daily dose will run at this UTC time.[/white]")
|
|
1302
|
+
new_time = Prompt.ask(f"[bold {colors['primary']}]Enter time in UTC (HH:MM)[/bold {colors['primary']}]", default=scheduled_time)
|
|
1303
|
+
try:
|
|
1304
|
+
datetime.strptime(new_time, "%H:%M")
|
|
1305
|
+
current_settings["scheduled_time"] = new_time
|
|
1306
|
+
all_settings["daily_dose"] = current_settings
|
|
1307
|
+
result = await api_client.update_settings(all_settings)
|
|
1308
|
+
if result.get("success"):
|
|
1309
|
+
print_success(console, f"Scheduled time set to {new_time} UTC")
|
|
1310
|
+
else:
|
|
1311
|
+
current_settings["scheduled_time"] = scheduled_time # Revert on failure
|
|
1312
|
+
print_error(console, f"Failed to update: {result.get('message', 'Unknown error')}")
|
|
1313
|
+
except ValueError:
|
|
1314
|
+
print_error(console, "Invalid time format. Use HH:MM")
|
|
1315
|
+
|
|
1316
|
+
elif choice == "3":
|
|
1317
|
+
new_max = IntPrompt.ask(f"[bold {colors['primary']}]Enter max papers (1-10)[/bold {colors['primary']}]", default=max_papers)
|
|
1318
|
+
if 1 <= new_max <= 10:
|
|
1319
|
+
current_settings["max_papers"] = new_max
|
|
1320
|
+
all_settings["daily_dose"] = current_settings
|
|
1321
|
+
result = await api_client.update_settings(all_settings)
|
|
1322
|
+
if result.get("success"):
|
|
1323
|
+
print_success(console, f"Max papers set to {new_max}")
|
|
1324
|
+
else:
|
|
1325
|
+
current_settings["max_papers"] = max_papers # Revert on failure
|
|
1326
|
+
print_error(console, f"Failed to update: {result.get('message', 'Unknown error')}")
|
|
1327
|
+
else:
|
|
1328
|
+
print_error(console, "Max papers must be between 1 and 10")
|
|
1329
|
+
|
|
1330
|
+
elif choice == "4":
|
|
1331
|
+
console.print(f"\n[bold {colors['primary']}]Current keywords:[/bold {colors['primary']}] {', '.join(keywords) if keywords else 'None'}")
|
|
1332
|
+
new_keywords_str = Prompt.ask(f"[bold {colors['primary']}]Enter keywords (space-separated)[/bold {colors['primary']}]", default=" ".join(keywords))
|
|
1333
|
+
new_keywords = [k.strip() for k in new_keywords_str.split() if k.strip()]
|
|
1334
|
+
current_settings["keywords"] = new_keywords
|
|
1335
|
+
all_settings["daily_dose"] = current_settings
|
|
1336
|
+
result = await api_client.update_settings(all_settings)
|
|
1337
|
+
if result.get("success"):
|
|
1338
|
+
print_success(console, f"Keywords updated: {' '.join(new_keywords)}")
|
|
1339
|
+
else:
|
|
1340
|
+
current_settings["keywords"] = keywords # Revert on failure
|
|
1341
|
+
print_error(console, f"Failed to update: {result.get('message', 'Unknown error')}")
|
|
1342
|
+
|
|
1343
|
+
elif choice == "5":
|
|
1344
|
+
show_command_suggestions(console, context='settings')
|
|
1345
|
+
break
|
|
1346
|
+
|
|
1347
|
+
# ================================
|
|
1348
|
+
# SAVE HELPER FUNCTIONS
|
|
1349
|
+
# ================================
|
|
1350
|
+
|
|
1351
|
+
async def _save_categories(user_id: str, categories: List[str]):
|
|
1352
|
+
"""Save categories and update MongoDB"""
|
|
1353
|
+
try:
|
|
1354
|
+
# Get current preferences
|
|
1355
|
+
prefs_result = await unified_user_service.get_user_preferences(user_id)
|
|
1356
|
+
prefs = prefs_result['preferences'] if prefs_result['success'] else {}
|
|
1357
|
+
|
|
1358
|
+
# Update categories
|
|
1359
|
+
prefs['categories'] = categories
|
|
1360
|
+
|
|
1361
|
+
# Save to preferences service (which handles MongoDB updates)
|
|
1362
|
+
result = await unified_user_service.update_user_preferences(user_id, prefs)
|
|
1363
|
+
|
|
1364
|
+
if result['success']:
|
|
1365
|
+
print_success(console, f"Categories updated: {', '.join(categories) if categories else 'None'}")
|
|
1366
|
+
else:
|
|
1367
|
+
print_error(console, "Failed to save categories")
|
|
1368
|
+
except Exception as e:
|
|
1369
|
+
print_error(console, f"Error saving categories: {e}")
|
|
1370
|
+
|
|
1371
|
+
async def _save_keywords(user_id: str, keywords: List[str], exclude_keywords: List[str]):
|
|
1372
|
+
"""Save keywords and update MongoDB"""
|
|
1373
|
+
try:
|
|
1374
|
+
prefs_result = await unified_user_service.get_user_preferences(user_id)
|
|
1375
|
+
prefs = prefs_result['preferences'] if prefs_result['success'] else {}
|
|
1376
|
+
|
|
1377
|
+
prefs['keywords'] = keywords
|
|
1378
|
+
prefs['exclude_keywords'] = exclude_keywords
|
|
1379
|
+
|
|
1380
|
+
result = await unified_user_service.update_user_preferences(user_id, prefs)
|
|
1381
|
+
|
|
1382
|
+
if result['success']:
|
|
1383
|
+
print_success(console, "Keywords updated successfully")
|
|
1384
|
+
else:
|
|
1385
|
+
print_error(console, "Failed to save keywords")
|
|
1386
|
+
except Exception as e:
|
|
1387
|
+
print_error(console, f"Error saving keywords: {e}")
|
|
1388
|
+
|
|
1389
|
+
async def _save_authors(user_id: str, authors: List[str]):
|
|
1390
|
+
"""Save authors and update MongoDB"""
|
|
1391
|
+
try:
|
|
1392
|
+
prefs_result = await unified_user_service.get_user_preferences(user_id)
|
|
1393
|
+
prefs = prefs_result['preferences'] if prefs_result['success'] else {}
|
|
1394
|
+
|
|
1395
|
+
prefs['authors'] = authors
|
|
1396
|
+
|
|
1397
|
+
result = await unified_user_service.update_user_preferences(user_id, prefs)
|
|
1398
|
+
|
|
1399
|
+
if result['success']:
|
|
1400
|
+
print_success(console, f"Authors updated: {', '.join(authors) if authors else 'None'}")
|
|
1401
|
+
else:
|
|
1402
|
+
print_error(console, "Failed to save authors")
|
|
1403
|
+
except Exception as e:
|
|
1404
|
+
print_error(console, f"Error saving authors: {e}")
|
|
1405
|
+
|
|
1406
|
+
if __name__ == "__main__":
|
|
1407
|
+
settings()
|