arionxiv 1.0.32__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. arionxiv/__init__.py +40 -0
  2. arionxiv/__main__.py +10 -0
  3. arionxiv/arxiv_operations/__init__.py +0 -0
  4. arionxiv/arxiv_operations/client.py +225 -0
  5. arionxiv/arxiv_operations/fetcher.py +173 -0
  6. arionxiv/arxiv_operations/searcher.py +122 -0
  7. arionxiv/arxiv_operations/utils.py +293 -0
  8. arionxiv/cli/__init__.py +4 -0
  9. arionxiv/cli/commands/__init__.py +1 -0
  10. arionxiv/cli/commands/analyze.py +587 -0
  11. arionxiv/cli/commands/auth.py +365 -0
  12. arionxiv/cli/commands/chat.py +714 -0
  13. arionxiv/cli/commands/daily.py +482 -0
  14. arionxiv/cli/commands/fetch.py +217 -0
  15. arionxiv/cli/commands/library.py +295 -0
  16. arionxiv/cli/commands/preferences.py +426 -0
  17. arionxiv/cli/commands/search.py +254 -0
  18. arionxiv/cli/commands/settings_unified.py +1407 -0
  19. arionxiv/cli/commands/trending.py +41 -0
  20. arionxiv/cli/commands/welcome.py +168 -0
  21. arionxiv/cli/main.py +407 -0
  22. arionxiv/cli/ui/__init__.py +1 -0
  23. arionxiv/cli/ui/global_theme_manager.py +173 -0
  24. arionxiv/cli/ui/logo.py +127 -0
  25. arionxiv/cli/ui/splash.py +89 -0
  26. arionxiv/cli/ui/theme.py +32 -0
  27. arionxiv/cli/ui/theme_system.py +391 -0
  28. arionxiv/cli/utils/__init__.py +54 -0
  29. arionxiv/cli/utils/animations.py +522 -0
  30. arionxiv/cli/utils/api_client.py +583 -0
  31. arionxiv/cli/utils/api_config.py +505 -0
  32. arionxiv/cli/utils/command_suggestions.py +147 -0
  33. arionxiv/cli/utils/db_config_manager.py +254 -0
  34. arionxiv/github_actions_runner.py +206 -0
  35. arionxiv/main.py +23 -0
  36. arionxiv/prompts/__init__.py +9 -0
  37. arionxiv/prompts/prompts.py +247 -0
  38. arionxiv/rag_techniques/__init__.py +8 -0
  39. arionxiv/rag_techniques/basic_rag.py +1531 -0
  40. arionxiv/scheduler_daemon.py +139 -0
  41. arionxiv/server.py +1000 -0
  42. arionxiv/server_main.py +24 -0
  43. arionxiv/services/__init__.py +73 -0
  44. arionxiv/services/llm_client.py +30 -0
  45. arionxiv/services/llm_inference/__init__.py +58 -0
  46. arionxiv/services/llm_inference/groq_client.py +469 -0
  47. arionxiv/services/llm_inference/llm_utils.py +250 -0
  48. arionxiv/services/llm_inference/openrouter_client.py +564 -0
  49. arionxiv/services/unified_analysis_service.py +872 -0
  50. arionxiv/services/unified_auth_service.py +457 -0
  51. arionxiv/services/unified_config_service.py +456 -0
  52. arionxiv/services/unified_daily_dose_service.py +823 -0
  53. arionxiv/services/unified_database_service.py +1633 -0
  54. arionxiv/services/unified_llm_service.py +366 -0
  55. arionxiv/services/unified_paper_service.py +604 -0
  56. arionxiv/services/unified_pdf_service.py +522 -0
  57. arionxiv/services/unified_prompt_service.py +344 -0
  58. arionxiv/services/unified_scheduler_service.py +589 -0
  59. arionxiv/services/unified_user_service.py +954 -0
  60. arionxiv/utils/__init__.py +51 -0
  61. arionxiv/utils/api_helpers.py +200 -0
  62. arionxiv/utils/file_cleanup.py +150 -0
  63. arionxiv/utils/ip_helper.py +96 -0
  64. arionxiv-1.0.32.dist-info/METADATA +336 -0
  65. arionxiv-1.0.32.dist-info/RECORD +69 -0
  66. arionxiv-1.0.32.dist-info/WHEEL +5 -0
  67. arionxiv-1.0.32.dist-info/entry_points.txt +4 -0
  68. arionxiv-1.0.32.dist-info/licenses/LICENSE +21 -0
  69. arionxiv-1.0.32.dist-info/top_level.txt +1 -0
@@ -0,0 +1,505 @@
1
+ """
2
+ API Key Configuration Manager for ArionXiv
3
+ Handles secure storage and retrieval of API keys (Gemini, HuggingFace, etc.)
4
+ """
5
+
6
+ import os
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Dict, Any, Optional
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.prompt import Prompt, Confirm
13
+ from rich.table import Table
14
+
15
+ console = Console()
16
+
17
+ # Import animation utility
18
+ try:
19
+ from ..utils.animations import left_to_right_reveal
20
+ ANIMATIONS_AVAILABLE = True
21
+ except ImportError:
22
+ ANIMATIONS_AVAILABLE = False
23
+ def left_to_right_reveal(console, text, style="", duration=1.0):
24
+ console.print(text, style=style)
25
+
26
+
27
+ # Step-by-step instructions for getting API keys
28
+ API_KEY_INSTRUCTIONS = {
29
+ "gemini": {
30
+ "title": "How to Get Your Google Gemini API Key (FREE)",
31
+ "steps": [
32
+ "1. Go to: https://aistudio.google.com/app/apikey",
33
+ "2. Sign in with your Google account",
34
+ "3. Click 'Create API Key'",
35
+ "4. Select a Google Cloud project (or create a new one)",
36
+ "5. Copy your API key",
37
+ "",
38
+ "Note: Gemini has a generous FREE tier - no credit card needed!"
39
+ ]
40
+ },
41
+ "huggingface": {
42
+ "title": "How to Get Your HuggingFace API Token (FREE)",
43
+ "steps": [
44
+ "1. Go to: https://huggingface.co/settings/tokens",
45
+ "2. Create a free account or sign in",
46
+ "3. Click 'New token'",
47
+ "4. Give it a name (e.g., 'ArionXiv')",
48
+ "5. Select 'Read' access (that's all we need)",
49
+ "6. Click 'Generate token' and copy it",
50
+ "",
51
+ "Note: HuggingFace is FREE for most models!"
52
+ ]
53
+ },
54
+ "groq": {
55
+ "title": "How to Get Your Groq API Key (FREE & FAST)",
56
+ "steps": [
57
+ "1. Go to: https://console.groq.com/keys",
58
+ "2. Create a free account or sign in",
59
+ "3. Click 'Create API Key'",
60
+ "4. Give it a name (e.g., 'ArionXiv')",
61
+ "5. Copy your API key",
62
+ "",
63
+ "Note: Groq is FREE and incredibly fast!",
64
+ " Optional - use for local LLM inference."
65
+ ]
66
+ },
67
+ "openrouter": {
68
+ "title": "How to Get Your OpenRouter API Key (FREE models available)",
69
+ "steps": [
70
+ "1. Go to: https://openrouter.ai/keys",
71
+ "2. Create a free account or sign in with Google/GitHub",
72
+ "3. Click 'Create Key'",
73
+ "4. Copy your API key (starts with sk-or-)",
74
+ "",
75
+ "Note: OpenRouter provides FREE access to many LLMs!",
76
+ " Recommended for paper chat feature.",
77
+ " Free models: Llama 3.3, Gemma, Qwen, and more."
78
+ ]
79
+ }
80
+ }
81
+
82
+
83
+ class APIConfigManager:
84
+ """
85
+ Manages API key configuration with secure local storage.
86
+
87
+ Keys are stored in ~/.arionxiv/api_keys.json
88
+ Environment variables take precedence over stored keys.
89
+ """
90
+
91
+ # Supported API providers
92
+ PROVIDERS = {
93
+ "gemini": {
94
+ "name": "Google Gemini",
95
+ "env_var": "GEMINI_API_KEY",
96
+ "description": "Used for embeddings and AI features (FREE tier available)",
97
+ "url": "https://aistudio.google.com/app/apikey",
98
+ "required": False
99
+ },
100
+ "huggingface": {
101
+ "name": "HuggingFace",
102
+ "env_var": "HF_API_KEY",
103
+ "description": "Used for model downloads and inference API",
104
+ "url": "https://huggingface.co/settings/tokens",
105
+ "required": False
106
+ },
107
+ "groq": {
108
+ "name": "Groq",
109
+ "env_var": "GROQ_API_KEY",
110
+ "description": "Optional - for local LLM inference (hosted API is used by default)",
111
+ "url": "https://console.groq.com/keys",
112
+ "required": False
113
+ },
114
+ "openrouter": {
115
+ "name": "OpenRouter",
116
+ "env_var": "OPENROUTER_API_KEY",
117
+ "description": "For paper chat - access FREE LLMs (Llama, Gemma, Qwen)",
118
+ "url": "https://openrouter.ai/keys",
119
+ "required": False
120
+ },
121
+ "openrouter_model": {
122
+ "name": "OpenRouter Model",
123
+ "env_var": "OPENROUTER_MODEL",
124
+ "description": "Model to use with OpenRouter (e.g., openai/gpt-4o-mini, meta-llama/llama-3.3-70b-instruct:free)",
125
+ "url": "https://openrouter.ai/models",
126
+ "required": False
127
+ }
128
+ }
129
+
130
+ def __init__(self):
131
+ self.config_dir = Path.home() / ".arionxiv"
132
+ self.api_keys_file = self.config_dir / "api_keys.json"
133
+ self._keys: Dict[str, str] = {}
134
+ self._loaded = False
135
+
136
+ # Ensure config directory exists
137
+ self.config_dir.mkdir(exist_ok=True)
138
+
139
+ def _load_keys(self) -> Dict[str, str]:
140
+ """Load API keys from file"""
141
+ if self._loaded:
142
+ return self._keys
143
+
144
+ try:
145
+ if self.api_keys_file.exists():
146
+ with open(self.api_keys_file, 'r') as f:
147
+ self._keys = json.load(f)
148
+ else:
149
+ self._keys = {}
150
+ except Exception:
151
+ self._keys = {}
152
+
153
+ self._loaded = True
154
+ return self._keys
155
+
156
+ def _save_keys(self) -> bool:
157
+ """Save API keys to file"""
158
+ try:
159
+ with open(self.api_keys_file, 'w') as f:
160
+ json.dump(self._keys, f, indent=2)
161
+
162
+ # Set restrictive permissions (owner read/write only)
163
+ try:
164
+ os.chmod(self.api_keys_file, 0o600)
165
+ except Exception:
166
+ pass # Windows may not support chmod
167
+
168
+ return True
169
+ except Exception as e:
170
+ console.print(f"[red]Error saving API keys: {e}[/red]")
171
+ return False
172
+
173
+ def get_api_key(self, provider: str) -> Optional[str]:
174
+ """
175
+ Get API key for a provider.
176
+ Environment variables take precedence over stored keys.
177
+ """
178
+ if provider not in self.PROVIDERS:
179
+ return None
180
+
181
+ env_var = self.PROVIDERS[provider]["env_var"]
182
+
183
+ # Check environment variable first
184
+ env_key = os.getenv(env_var)
185
+ if env_key:
186
+ return env_key
187
+
188
+ # Fall back to stored key
189
+ self._load_keys()
190
+ return self._keys.get(provider)
191
+
192
+ def set_api_key(self, provider: str, key: str) -> bool:
193
+ """Set API key for a provider"""
194
+ if provider not in self.PROVIDERS:
195
+ return False
196
+
197
+ self._load_keys()
198
+ self._keys[provider] = key
199
+
200
+ # Also set as environment variable for current session
201
+ env_var = self.PROVIDERS[provider]["env_var"]
202
+ os.environ[env_var] = key
203
+
204
+ return self._save_keys()
205
+
206
+ def remove_api_key(self, provider: str) -> bool:
207
+ """Remove API key for a provider"""
208
+ if provider not in self.PROVIDERS:
209
+ return False
210
+
211
+ self._load_keys()
212
+ if provider in self._keys:
213
+ del self._keys[provider]
214
+ return self._save_keys()
215
+ return True
216
+
217
+ def is_configured(self, provider: str) -> bool:
218
+ """Check if a provider's API key is configured"""
219
+ return self.get_api_key(provider) is not None
220
+
221
+ def get_status(self) -> Dict[str, Dict[str, Any]]:
222
+ """Get configuration status for all providers"""
223
+ status = {}
224
+ for provider, info in self.PROVIDERS.items():
225
+ key = self.get_api_key(provider)
226
+ status[provider] = {
227
+ "name": info["name"],
228
+ "configured": key is not None,
229
+ "source": "environment" if os.getenv(info["env_var"]) else ("stored" if key else "not set"),
230
+ "required": info["required"],
231
+ "masked_key": self._mask_key(key) if key else None
232
+ }
233
+ return status
234
+
235
+ def _mask_key(self, key: str) -> str:
236
+ """Mask API key for display (show first 4 and last 4 chars)"""
237
+ if not key or len(key) < 12:
238
+ return "****"
239
+ return f"{key[:4]}...{key[-4:]}"
240
+
241
+ def is_first_time_setup_needed(self) -> bool:
242
+ """Check if first-time API setup is needed"""
243
+ # Check if setup has been completed or skipped
244
+ self._load_keys()
245
+ if self._keys.get("_setup_completed"):
246
+ return False
247
+
248
+ # Check if at least the required key (Groq) is set
249
+ if self.is_configured("groq"):
250
+ return False
251
+
252
+ return True
253
+
254
+ def mark_setup_completed(self) -> bool:
255
+ """Mark first-time setup as completed (even if skipped)"""
256
+ self._load_keys()
257
+ self._keys["_setup_completed"] = True
258
+ return self._save_keys()
259
+
260
+ def load_keys_to_environment(self):
261
+ """Load stored keys into environment variables and refresh clients"""
262
+ self._load_keys()
263
+ for provider, key in self._keys.items():
264
+ if provider.startswith("_"):
265
+ continue # Skip internal flags
266
+ if provider in self.PROVIDERS:
267
+ env_var = self.PROVIDERS[provider]["env_var"]
268
+ if key and not os.getenv(env_var):
269
+ os.environ[env_var] = key
270
+
271
+ # Refresh OpenRouter client to pick up the loaded keys
272
+ try:
273
+ from ...services.llm_inference.openrouter_client import openrouter_client
274
+ if openrouter_client:
275
+ openrouter_client.refresh_api_key()
276
+ except ImportError:
277
+ pass # OpenRouter client not available
278
+
279
+
280
+ # Global instance
281
+ api_config_manager = APIConfigManager()
282
+
283
+
284
+ def _show_api_instructions(console_instance: Console, provider: str, colors: Dict[str, str]):
285
+ """Display step-by-step instructions for getting an API key"""
286
+ if provider not in API_KEY_INSTRUCTIONS:
287
+ return
288
+
289
+ instructions = API_KEY_INSTRUCTIONS[provider]
290
+ steps_text = "\n".join(instructions["steps"])
291
+
292
+ left_to_right_reveal(console_instance, "", duration=0.3)
293
+ console_instance.print(Panel(
294
+ steps_text,
295
+ title=f"[bold {colors['primary']}]{instructions['title']}[/bold {colors['primary']}]",
296
+ border_style=f"bold {colors['primary']}",
297
+ padding=(1, 2)
298
+ ))
299
+
300
+
301
+ def run_first_time_api_setup(console_instance: Console = None) -> bool:
302
+ """
303
+ Run first-time API key setup wizard.
304
+ Returns True if setup was completed, False if skipped.
305
+ """
306
+ if console_instance is None:
307
+ console_instance = console
308
+
309
+ from ..ui.theme import get_theme_colors, style_text
310
+ colors = get_theme_colors()
311
+
312
+ console_instance.print()
313
+ console_instance.print(Panel(
314
+ "[bold]Welcome to ArionXiv![/bold]\n\n"
315
+ "ArionXiv is ready to use. All AI features work out of the box\n"
316
+ "using the hosted backend.\n\n"
317
+ "[bold]Optional:[/bold] Configure your own API keys for:\n"
318
+ " - Gemini - Enhanced embeddings\n"
319
+ " - Groq - Faster local LLM inference\n"
320
+ " - HuggingFace - Model downloads\n\n"
321
+ "[dim]You can configure these anytime with: arionxiv settings api[/dim]",
322
+ title="[bold]First-Time Setup[/bold]",
323
+ border_style=f"bold {colors['primary']}"
324
+ ))
325
+
326
+ # Ask if user wants to configure now - default is No
327
+ if not Confirm.ask(
328
+ f"\n[bold {colors['primary']}]Would you like to configure optional API keys now?[/bold {colors['primary']}]",
329
+ default=False
330
+ ):
331
+ left_to_right_reveal(console_instance, f"\nGreat! You're all set.", style=colors['primary'], duration=1.0)
332
+ left_to_right_reveal(console_instance, f"Configure keys later with: arionxiv settings api", style=f"dim {colors['primary']}", duration=1.0)
333
+ api_config_manager.mark_setup_completed()
334
+ return False
335
+
336
+ console_instance.print()
337
+
338
+ # Configure each provider with instructions
339
+ for provider, info in api_config_manager.PROVIDERS.items():
340
+ _configure_single_provider_with_instructions(console_instance, provider, info, colors, first_time=True)
341
+
342
+ api_config_manager.mark_setup_completed()
343
+
344
+ console_instance.print()
345
+ console_instance.print(Panel(
346
+ "API configuration complete!\n\n"
347
+ f"Manage keys anytime with: [{colors['primary']}]arionxiv settings api[/{colors['primary']}]",
348
+ title="[bold]Setup Complete[/bold]",
349
+ border_style=f"bold {colors['primary']}"
350
+ ))
351
+
352
+ return True
353
+
354
+
355
+ def _configure_single_provider_with_instructions(
356
+ console_instance: Console,
357
+ provider: str,
358
+ info: Dict[str, Any],
359
+ colors: Dict[str, str],
360
+ first_time: bool = False,
361
+ show_instructions: bool = True
362
+ ) -> bool:
363
+ """Configure a single API provider with step-by-step instructions"""
364
+
365
+ current_key = api_config_manager.get_api_key(provider)
366
+ required_text = "REQUIRED" if info["required"] else "optional"
367
+ req_style = colors['error'] if info["required"] else colors['primary']
368
+
369
+ left_to_right_reveal(console_instance, f"\n{'='*60}", style=colors['primary'], duration=0.5)
370
+ left_to_right_reveal(console_instance, f"{info['name']} ({required_text})", style=f"bold {colors['primary']}", duration=0.8)
371
+ left_to_right_reveal(console_instance, f"{info['description']}", style="white", duration=0.6)
372
+
373
+ if current_key:
374
+ left_to_right_reveal(console_instance, f"Already configured: {api_config_manager._mask_key(current_key)}", style=f"bold {colors['primary']}", duration=0.8)
375
+ if first_time:
376
+ # Already configured, skip
377
+ return True
378
+
379
+ # Show step-by-step instructions
380
+ if show_instructions and provider in API_KEY_INSTRUCTIONS:
381
+ _show_api_instructions(console_instance, provider, colors)
382
+
383
+ # Ask for key
384
+ prompt_text = f"Enter {info['name']} API key"
385
+ if not info["required"]:
386
+ prompt_text += " (or press Enter to skip)"
387
+
388
+ key_input = Prompt.ask(
389
+ f"\n[bold {colors['primary']}]{prompt_text}[/bold {colors['primary']}]",
390
+ default="",
391
+ show_default=False
392
+ )
393
+
394
+ if key_input.strip():
395
+ if api_config_manager.set_api_key(provider, key_input.strip()):
396
+ left_to_right_reveal(console_instance, f"{info['name']} key saved successfully!", style=f"bold {colors['primary']}", duration=1.0)
397
+ return True
398
+ else:
399
+ left_to_right_reveal(console_instance, f"Failed to save {info['name']} key", style=f"bold {colors['error']}", duration=1.0)
400
+ return False
401
+ else:
402
+ if info["required"]:
403
+ left_to_right_reveal(console_instance, f"Warning: {info['name']} key is REQUIRED for AI features", style=colors['warning'], duration=1.0)
404
+ left_to_right_reveal(console_instance, f" You can add it later with: arionxiv settings api", style=colors['warning'], duration=0.8)
405
+ else:
406
+ left_to_right_reveal(console_instance, f"Skipped {info['name']} (you can add it later)", style="white", duration=0.8)
407
+ return True
408
+
409
+
410
+ def _configure_single_provider(
411
+ console_instance: Console,
412
+ provider: str,
413
+ info: Dict[str, Any],
414
+ colors: Dict[str, str],
415
+ first_time: bool = False
416
+ ) -> bool:
417
+ """Configure a single API provider (legacy - without detailed instructions)"""
418
+
419
+ current_key = api_config_manager.get_api_key(provider)
420
+ required_text = "[required]" if info["required"] else "[optional]"
421
+
422
+ left_to_right_reveal(console_instance, f"\n{info['name']} {required_text}", style=f"bold {colors['primary']}", duration=0.8)
423
+ left_to_right_reveal(console_instance, f"{info['description']}", style="white", duration=0.6)
424
+ left_to_right_reveal(console_instance, f"Get your key at: {info['url']}", style="white", duration=0.6)
425
+
426
+ if current_key:
427
+ left_to_right_reveal(console_instance, f"Current: {api_config_manager._mask_key(current_key)}", style=f"bold {colors['primary']}", duration=0.8)
428
+ if first_time:
429
+ # Already configured, skip
430
+ return True
431
+
432
+ # Ask for key
433
+ prompt_text = f"Enter {info['name']} API key"
434
+ if not info["required"]:
435
+ prompt_text += " (or press Enter to skip)"
436
+
437
+ key_input = Prompt.ask(
438
+ f"[bold {colors['primary']}]{prompt_text}[/bold {colors['primary']}]",
439
+ default="",
440
+ show_default=False
441
+ )
442
+
443
+ if key_input.strip():
444
+ if api_config_manager.set_api_key(provider, key_input.strip()):
445
+ left_to_right_reveal(console_instance, f"{info['name']} key saved successfully", style=f"bold {colors['primary']}", duration=1.0)
446
+ return True
447
+ else:
448
+ left_to_right_reveal(console_instance, f"Failed to save {info['name']} key", style=f"bold {colors['error']}", duration=1.0)
449
+ return False
450
+ else:
451
+ if info["required"]:
452
+ left_to_right_reveal(console_instance, f"Warning: {info['name']} key is required for AI features", style=f"bold {colors['warning']}", duration=1.0)
453
+ else:
454
+ left_to_right_reveal(console_instance, f"Skipped {info['name']}", style="white", duration=0.8)
455
+ return True
456
+
457
+
458
+ def show_api_status(console_instance: Console = None):
459
+ """Display current API configuration status"""
460
+ if console_instance is None:
461
+ console_instance = console
462
+
463
+ from ..ui.theme import get_theme_colors
464
+ colors = get_theme_colors()
465
+
466
+ status = api_config_manager.get_status()
467
+
468
+ table = Table(
469
+ title="API Configuration Status",
470
+ show_header=True,
471
+ header_style=f"bold {colors['primary']}",
472
+ border_style=f"bold {colors['primary']}"
473
+ )
474
+ table.add_column("Provider", style="bold white")
475
+ table.add_column("Status", style="white", width=12)
476
+ table.add_column("Source", style="white", width=12)
477
+ table.add_column("Key", style="white", width=20)
478
+ table.add_column("Required", style="white", width=10)
479
+
480
+ for provider, info in status.items():
481
+ status_text = f"[bold {colors['primary']}]Configured[/bold {colors['primary']}]" if info["configured"] else f"[bold {colors['warning']}]Not Set[/bold {colors['warning']}]"
482
+ source_text = info["source"].title() if info["configured"] else "-"
483
+ key_text = info["masked_key"] if info["masked_key"] else "-"
484
+ required_text = "Yes" if info["required"] else "No"
485
+
486
+ table.add_row(
487
+ info["name"],
488
+ status_text,
489
+ source_text,
490
+ key_text,
491
+ required_text
492
+ )
493
+
494
+ console_instance.print()
495
+ console_instance.print(table)
496
+ console_instance.print()
497
+
498
+
499
+ __all__ = [
500
+ 'APIConfigManager',
501
+ 'api_config_manager',
502
+ 'run_first_time_api_setup',
503
+ 'show_api_status',
504
+ 'API_KEY_INSTRUCTIONS'
505
+ ]
@@ -0,0 +1,147 @@
1
+ """
2
+ Command Suggestions Utility
3
+ Shows helpful next commands after each feature completes
4
+ """
5
+
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from typing import List, Tuple, Optional
9
+
10
+
11
+ def _get_theme_colors():
12
+ """Get theme colors with lazy import to avoid circular imports"""
13
+ try:
14
+ from ..ui.theme_system import get_theme_colors
15
+ return get_theme_colors()
16
+ except ImportError:
17
+ return {'primary': 'cyan', 'secondary': 'blue', 'success': 'green',
18
+ 'warning': 'yellow', 'error': 'red', 'muted': 'dim'}
19
+
20
+
21
+ # Command categories for different contexts
22
+ CHAT_COMMANDS = [
23
+ ("arionxiv chat", "Start a new chat session"),
24
+ ("arionxiv search <query>", "Search for more papers"),
25
+ ("arionxiv settings papers", "Manage your saved papers"),
26
+ ]
27
+
28
+ SEARCH_COMMANDS = [
29
+ ("arionxiv chat", "Chat with a paper"),
30
+ ("arionxiv search <query>", "Search for different papers"),
31
+ ("arionxiv trending", "See trending papers"),
32
+ ]
33
+
34
+ SETTINGS_COMMANDS = [
35
+ ("arionxiv settings", "Back to settings menu"),
36
+ ("arionxiv chat", "Start a chat session"),
37
+ ("arionxiv search <query>", "Search for papers"),
38
+ ]
39
+
40
+ TRENDING_COMMANDS = [
41
+ ("arionxiv chat", "Chat with a paper"),
42
+ ("arionxiv search <query>", "Search for specific papers"),
43
+ ("arionxiv daily", "Get your daily digest"),
44
+ ]
45
+
46
+ DAILY_COMMANDS = [
47
+ ("arionxiv chat", "Chat with a paper"),
48
+ ("arionxiv search <query>", "Search for papers"),
49
+ ("arionxiv trending", "See trending papers"),
50
+ ]
51
+
52
+ ANALYZE_COMMANDS = [
53
+ ("arionxiv chat", "Chat with this paper"),
54
+ ("arionxiv search <query>", "Search for related papers"),
55
+ ("arionxiv settings papers", "Save to your library"),
56
+ ]
57
+
58
+ LIBRARY_COMMANDS = [
59
+ ("arionxiv chat", "Chat with a saved paper"),
60
+ ("arionxiv search <query>", "Find new papers"),
61
+ ("arionxiv settings papers", "Manage saved papers"),
62
+ ]
63
+
64
+ GENERAL_COMMANDS = [
65
+ ("arionxiv chat", "Chat with papers using AI"),
66
+ ("arionxiv search <query>", "Search arXiv papers"),
67
+ ("arionxiv trending", "See trending papers"),
68
+ ("arionxiv daily", "Daily paper digest"),
69
+ ("arionxiv settings", "Configure preferences"),
70
+ ]
71
+
72
+ # Navigation commands
73
+ NAVIGATION_COMMANDS = [
74
+ ("arionxiv", "Go to homepage"),
75
+ ("arionxiv --help", "Show all commands"),
76
+ ]
77
+
78
+
79
+ def show_command_suggestions(
80
+ console: Console,
81
+ context: str = "general",
82
+ custom_commands: Optional[List[Tuple[str, str]]] = None,
83
+ show_navigation: bool = True,
84
+ title: str = "What's Next?"
85
+ ):
86
+ """
87
+ Show helpful command suggestions after a feature completes.
88
+
89
+ Args:
90
+ console: Rich console instance
91
+ context: One of 'chat', 'search', 'settings', 'trending', 'daily',
92
+ 'analyze', 'library', 'general'
93
+ custom_commands: Optional list of (command, description) tuples to show instead
94
+ show_navigation: Whether to show navigation commands (homepage, help)
95
+ title: Panel title
96
+ """
97
+ colors = _get_theme_colors()
98
+
99
+ # Get commands based on context
100
+ context_commands = {
101
+ 'chat': CHAT_COMMANDS,
102
+ 'search': SEARCH_COMMANDS,
103
+ 'settings': SETTINGS_COMMANDS,
104
+ 'trending': TRENDING_COMMANDS,
105
+ 'daily': DAILY_COMMANDS,
106
+ 'analyze': ANALYZE_COMMANDS,
107
+ 'library': LIBRARY_COMMANDS,
108
+ 'general': GENERAL_COMMANDS,
109
+ }
110
+
111
+ commands = custom_commands or context_commands.get(context, GENERAL_COMMANDS)
112
+
113
+ # Build command lines
114
+ lines = []
115
+ for cmd, desc in commands:
116
+ lines.append(
117
+ f" [bold {colors['primary']}]{cmd}[/bold {colors['primary']}] "
118
+ f"[white]→ {desc}[/white]"
119
+ )
120
+
121
+ # Add separator and navigation if requested
122
+ if show_navigation:
123
+ lines.append("") # Empty line as separator
124
+ lines.append(f" [white]─────────────────────────────────────[/white]")
125
+ for cmd, desc in NAVIGATION_COMMANDS:
126
+ lines.append(
127
+ f" [bold {colors['primary']}]{cmd}[/bold {colors['primary']}] "
128
+ f"[white]→ {desc}[/white]"
129
+ )
130
+
131
+ console.print()
132
+ console.print(Panel(
133
+ "\n".join(lines),
134
+ title=f"[bold {colors['primary']}]{title}[/bold {colors['primary']}]",
135
+ border_style=f"bold {colors['primary']}",
136
+ padding=(1, 2)
137
+ ))
138
+
139
+
140
+ def show_back_to_home(console: Console):
141
+ """Show a simple message about going back to homepage"""
142
+ colors = _get_theme_colors()
143
+ console.print()
144
+ console.print(
145
+ f"[white]Run [bold {colors['primary']}]arionxiv[/bold {colors['primary']}] "
146
+ f"to go back to homepage[/white]"
147
+ )