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,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
|
+
)
|